Upgrade to Pro — share decks privately, control downloads, hide ads and more …

What's New in Rails 5?

karol.galanciak
September 28, 2016
190

What's New in Rails 5?

karol.galanciak

September 28, 2016
Tweet

Transcript

  1. What's new in Rails 5?
    Karol Galanciak
    Full Stack Developer at
    Twitter: @Azdaroth
    Github: Azdaroth
    https://karolgalanciak.com

    View Slide

  2. ActionCable

    View Slide

  3. ActionCable
    • Integration of WebSockets with the rest of your
    application

    View Slide

  4. ActionCable
    • Integration of WebSockets with the rest of your
    application
    • Both client-side and server-side tools

    View Slide

  5. ActionCable - server-side

    View Slide

  6. ActionCable - server-side
    • Connections - connection between consumer and
    the sever

    View Slide

  7. ActionCable - server-side
    • Connections - connection between consumer and
    the sever
    • Authentication / authorization goes here

    View Slide

  8. ActionCable - connections
    module ApplicationCable
    class Connection < ActionCable::Connection::Base
    identified_by :current_user
    def connect
    self.current_user = find_user!
    end
    private
    def find_user!
    User.find_by(id: cookies.signed[:user_id]) or raise UnauthorizedConnectionError
    end
    end
    end

    View Slide

  9. ActionCable - server-side
    • Channels - endpoints where the actual logic
    happens (~ controllers)

    View Slide

  10. ActionCable - channels
    class ChatChannel < ActionCable::Channel::Base
    def subscribed
    stream_from "chat_#{params[:room]}"
    end
    def receive(payload)
    ActionCable.server.broadcast("chat_#{params[:room]}", payload)
    end
    end

    View Slide

  11. ActionCable - client-side

    View Slide

  12. ActionCable - client-side
    //= require action_cable
    this.App || (this.App = {});
    App.cable = ActionCable.createConsumer();
    App.chatChannel = App.cable.subscriptions.create({
    channel: "ChatChannel",
    room: "Rails Core"
    }, {
    received: function(data) {
    $(".chat").append("" + data["sent_by"] + ": " + data["body"] + "")
    }
    });
    App.chatChannel.send({
    sent_by: "DHH",
    body: "OMG, it works!"
    });

    View Slide

  13. Rails API

    View Slide

  14. Rails API
    • rails new rails_5_app --api

    View Slide

  15. Rails API
    • rails new rails_5_app —api
    • ApplicationController inherits from
    ActionController::API
    • Modifies the generators (no views, helpers and
    assets)

    View Slide

  16. Attributes API

    View Slide

  17. Attributes API
    class Order < ActiveRecord::Base
    attribute :price_in_cents, :money
    attribute :description, :string, default: "Default description"
    attribute :ordered_at, :datetime, default: -> { Time.current }
    attribute :tags_not_backed_by_db, :string, array: true
    end
    class MoneyType < ActiveRecord::Type::Integer
    def deserialize(value)
    if value.to_s.include?("$")
    price_in_dollars = value.gsub(/\$/, "").to_d
    super(price_in_dollars * 100)
    else
    super
    end
    end
    end
    ActiveRecord::Type.register(:money, MoneyType)

    View Slide

  18. Attributes API
    class Order < ActiveRecord::Base
    attribute :price_in_cents, :money
    attribute :description, :string, default: "Default description"
    attribute :ordered_at, :datetime, default: -> { Time.current }
    attribute :tags_not_backed_by_db, :string, array: true
    end
    order = Order.new
    order.price_in_cents = "$10.0"
    order.price_in_cents
    => 1000
    order.description
    => "Default description"
    order.ordered_at
    => "2016-01-01 12:00:00 +0200"
    order.tags_not_backed_by_db = ["WhateverItTakes", "5%", "Rich Piana"]
    order.tags_not_backed_by_db
    => ["WhateverItTakes", "5%", "Rich Piana"]

    View Slide

  19. Attributes API
    • custom types (requires #deserialize method for
    assigning values and #serialize when querying
    database)

    View Slide

  20. Attributes API
    • custom types (requires #deserialize method for
    assigning values and #serialize when querying
    database)
    • attributes don’t need to be backed by database

    View Slide

  21. Attributes API
    • custom types (requires #deserialize method for
    assigning values and #serialize when querying
    database)
    • attributes don’t need to be backed by database
    • supports dirty tracking

    View Slide

  22. Class / module variables per
    thread
    • cattr_accessor / mattr_accessor - but per thread -
    thread_cattr_accessor / thread_mattr_accessor

    View Slide

  23. Class / module variables per
    thread
    module OmgGlobal
    thread_mattr_accessor :current_user
    end
    class ApplicationController < ActionController::Base
    # Please, don't do it in real code!
    before_action :set_current_user_globally
    def set_current_user_globally
    OmgGlobal.current_user = current_user
    end
    end
    class Post < ActiveRecord::Base
    after_save :create_version
    private
    def create_version
    Version.create(model: self.class.to_s, attributes: attributes, who_did_it:
    OmgGlobal.current_user.id)
    end
    end

    View Slide

  24. Assets silenced by default in
    development

    View Slide

  25. Assets silenced by default in
    development
    • quite_assets gem

    View Slide

  26. Assets silenced by default in
    development
    • quite_assets gem
    • No more garbage in logs

    View Slide

  27. Assets silenced by default in
    development
    • quite_assets gem
    • No more garbage in logs
    Started GET "/assets/public/home.js?body=1" for 127.0.0.1 at 2016-09-27 19:31:46 +0200
    Started GET "/assets/bootstrap/collapse.js?body=1" for 127.0.0.1 at 2016-09-27 19:32:04
    +0200
    Started GET "/assets/font-awesome/fontawesome-webfont.woff2?v=4.5.0" for 127.0.0.1 at
    2016-09-27 19:32:04 +0200
    Started GET "/assets/public/locales.js?body=1" for 127.0.0.1 at 2016-09-27 19:32:04 +0200

    View Slide

  28. ApplicationRecord

    View Slide

  29. ApplicationRecord
    • New base class for models

    View Slide

  30. ApplicationRecord
    class ApplicationRecord < ActiveRecord::Base
    include SoftDelete
    self.abstract_class = true
    has_paper_trail
    end
    class User < ApplicationRecord
    end

    View Slide

  31. Rendering outside controllers

    View Slide

  32. Rendering outside controllers
    OrdersController.render(:show, assigns: { order: Order.find(order_id) })

    View Slide

  33. Rendering outside controllers
    OrdersController.render(:show, assigns: { order: Order.find(order_id) })
    OrdersController.render(:_order, assigns: { order: Order.find(order_id) })

    View Slide

  34. Rake / Rails commands

    View Slide

  35. Rake / Rails commands
    • All rails commands can be executed with rake and
    all rake commands can be executed with rails

    View Slide

  36. Rake / Rails commands
    rails db:migrate
    rake routes | grep some_route -> rails routes -g some_route

    View Slide

  37. Caching collections

    View Slide

  38. Caching collections
    • ActiveRecord::Collection#cache_key

    View Slide

  39. Caching collections
    posts.cache_key
    => "posts/query-c60f5cf40b4d0d6f492ea6961c43ceb6-100-20160927121435182129"

    View Slide

  40. Caching collections
    <% cache(@posts) do %>
    <% @posts.each do |post| %>
    <%= render post %>
    <% end %>
    <% end %>

    View Slide

  41. ActiveRecord no longer halts execution
    of the callbacks when returning false

    View Slide

  42. ActiveRecord no longer halts execution
    of the callbacks when returning false
    class Product < ActiveRecord::Base
    before_save :set_availability_status
    def set_availability_status
    self.available = somehow_compute_availability_status_which_returns_false
    end
    end
    product.save!
    => ActiveRecord::RecordNotSaved: ActiveRecord::RecordNotSaved

    View Slide

  43. ActiveRecord no longer halts execution
    of the callbacks when returning false
    class Product < ActiveRecord::Base
    before_save :set_availability_status
    def set_availability_status
    self.available = somehow_compute_availability_status_which_returns_false
    throw(:abort)
    end
    end

    View Slide

  44. ActiveRecord no longer halts execution
    of the callbacks when returning false
    • configurable with
    ActiveSupport.halt_callback_chains_on_return_false

    View Slide

  45. Belongs-to association
    required by default

    View Slide

  46. Belongs-to association
    required by default
    class Comment < ApplicationRecord
    belongs_to :author
    end

    View Slide

  47. Belongs-to association
    required by default
    class Comment < ApplicationRecord
    belongs_to :author, optional: true
    end

    View Slide

  48. Belongs-to association
    required by default
    • Configurable with
    Rails.application.config.active_record.belongs_to_r
    equired_by_default

    View Slide

  49. redirect_back method

    View Slide

  50. redirect_back method
    class UsersController < ApplicationController
    def update
    user = find_user
    user.update(params[:user])
    redirect_to :back
    end
    end

    View Slide

  51. redirect_back method
    class UsersController < ApplicationController
    def update
    user = find_user
    user.update(params[:user])
    redirect_back(fallback_location: root_path)
    end
    end

    View Slide

  52. ActiveRecord::Base.Suppress

    View Slide

  53. ActiveRecord::Base.Suppress

    View Slide

  54. ActiveRecord::Base.Suppress
    class Comment < ApplicationRecord
    belongs_to :author
    has_many :notifications
    after_save :send_notification
    def publish!
    update!(published_at: Time.current)
    end
    private
    def send_notification
    notifications.create(message: content, author: author)
    end
    end

    View Slide

  55. ActiveRecord::Base.Suppress
    class Comment < ApplicationRecord
    belongs_to :author
    has_many :notifications
    after_save :send_notification
    def publish!
    update!(published_at: Time.current)
    end
    def publish_without_notification!
    Notification.suppress do
    publish!
    end
    end
    private
    def send_notification
    notifications.create(message: content, author: author)
    end
    end

    View Slide

  56. Left Outer Join Support

    View Slide

  57. Left Outer Join Support
    User.left_outer_joins(:posts, :comments, :notifications)
    User.left_joins(:posts, :comments, :notifications)

    View Slide

  58. Config for UUIDs as primary keys

    View Slide

  59. Config for UUIDs as primary keys
    create_table :orders, id: :uuid do |t|
    end

    View Slide

  60. Config for UUIDs as primary keys
    • Configurable with:
    config.active_record.primary_key = :uuid

    View Slide

  61. Queue adapter configurable
    per job

    View Slide

  62. Queue adapter configurable
    per job
    config.active_job.queue_adapter = :delayed_job

    View Slide

  63. Queue adapter configurable
    per job
    class DoSomethingExpensiveInBackgroundJob < ActiveJob::Base
    self.queue_adapter = :sidekiq
    end

    View Slide

  64. ApplicationJob base class

    View Slide

  65. ApplicationJob base class
    class ApplicationJob < ActiveJob::Base
    end

    View Slide

  66. Validation of multiple contexts

    View Slide

  67. Validation of multiple contexts
    class User < ActiveRecord::Base
    validate :validate_confirmation_info, on: :confirmation
    validate :validate_updatable, on: :update
    end

    View Slide

  68. Validation of multiple contexts
    • Before:
    user.valid?(:confirmation) && user.valid?(:update)

    View Slide

  69. Validation of multiple contexts
    • Now:
    user.valid?([:confirmation, :update])

    View Slide

  70. Additional details for
    validations

    View Slide

  71. Additional details for
    validations
    comment = Comment.new
    comment.valid? => false
    comment.errors.messages => {content: ["can’t be blank"]}
    comment.errors.details => {content:=>[{error: :blank}]}
    comment.errors.add(:content, :too_short, "Content is too short")
    comment.errors.details => {content: [{error: :blank}, {error: :too_short}]}

    View Slide

  72. Saving records without
    timestamp update

    View Slide

  73. Saving records without
    timestamp update
    user = User.first
    user.save(touch: false)

    View Slide

  74. Bidirectional destroy

    View Slide

  75. Bidirectional destroy
    • Doesn’t cause SystemStackError

    View Slide

  76. Bidirectional destroy
    class Account < ActiveRecord::Base
    has_one :billing_address, dependent: :destroy
    end
    class BillingAddress < ActiveRecord::Base
    belongs_to :account, dependent: :destroy
    end

    View Slide

  77. ActiveModel::AttributesAssi
    gnment

    View Slide

  78. ActiveModel::AttributesAssi
    gnment
    class UserForm
    include ActiveModel::AttributeAssignment
    attr_accessor :email, :password, :password_confirmation
    end
    form = UserForm.new
    form.assign_attributes(email: "[email protected]")

    View Slide

  79. ActionController::Parameters is
    not a HashWithIndifferentAccess

    View Slide

  80. ActionController::Parameters is
    not a HashWithIndifferentAccess
    • Gotcha:
    def to_h
    if permitted?
    @parameters.to_h
    else
    slice(*self.class.always_permitted_parameters).permit!.to_h
    end
    end

    View Slide

  81. ActionController::Parameters is
    not a HashWithIndifferentAccess
    • Gotcha:
    def to_h
    if permitted?
    @parameters.to_h
    else
    slice(*self.class.always_permitted_parameters).permit!.to_h
    end
    end
    def user_params
    params.permit!.to_h
    end
    def user_params
    params.to_unsafe_h
    end

    View Slide

  82. Comments in migrations

    View Slide

  83. Comments in migrations
    class CreateOrders < ActiveRecord::Migration[5.0]
    def change
    create_table :orders, comment: "Orders table" do |t|
    t.string :description, comment: "Description of the order"
    t.integer :owner_id, comment: "Owner of the order"
    t.timestamps
    end
    add_index :orders, :owner_id, name: "index_name", comment: "Index for owner_id"
    end
    end

    View Slide

  84. Comments in migrations
    ActiveRecord::Schema.define(version: 20160928091043) do
    create_table "orders", force: :cascade, comment: "Orders table" do |t|
    t.string "description", comment: "Description of the order"
    t.integer "owner_id", comment: "Owner of the order"
    t.index ["owner_id"], name: "index_name",using: :btree, comment: "Index for owner_id"
    end
    end

    View Slide

  85. Updating collection with
    callbacks and validations

    View Slide

  86. Updating collection with
    callbacks and validations
    • ActiveRecord::Relation.update_all doesn’t trigger
    validations and callbacks

    View Slide

  87. Updating collection with
    callbacks and validations
    • ActiveRecord::Relation.update_all doesn’t trigger
    validations and callbacks
    • But new ActiveRecord::Relation.update does ;)

    View Slide

  88. Updating collection with
    callbacks and validations
    • ActiveRecord::Relation.update_all doesn’t trigger
    validations and callbacks
    • But new ActiveRecord::Relation.update does ;)
    Comment.limit(10).update(content: "")

    View Slide

  89. Expression indexes

    View Slide

  90. Expression indexes
    Author.where("lower(fullname) = ?", fullname.to_s.downcase)

    View Slide

  91. Expression indexes
    add_index :authors, "lower(fullname)"

    View Slide

  92. Indexed errors for nested
    attributes

    View Slide

  93. Indexed errors for nested
    attributes
    class Invoice < ApplicationRecord
    has_many :items
    accepts_nested_attributes_for :items
    end
    class Item < ApplicationRecord
    validates :description, presence: true
    end
    > invoice = Invoice.new
    > item_1 = Item.new(description: "$$$$")
    > item_2 = Item.new
    > invoice.items = [item_1, item_2]
    > invoice.save
    > invoice.error.messages
    => {:"items.description"=>["can't be blank"]}

    View Slide

  94. Indexed errors for nested
    attributes
    class Invoice < ApplicationRecord
    has_many :items, index_errors: true
    accepts_nested_attributes_for :items
    end
    class Item < ApplicationRecord
    validates :description, presence: true
    end
    >> invoice = Invoice.new
    >> item_1 = Item.new(description: "$$$$")
    >> item_2 = Item.new
    >> invoice.items = [item_1, item_2]
    >> invoice.save
    >> invoice.error.messages
    => {:"items[1].description"=>["can’t be blank"]}

    View Slide

  95. Indexed errors for nested
    attributes
    • Configurable with
    config.active_record.index_nested_attribute_errors

    View Slide

  96. No more ambiguous
    columns errors

    View Slide

  97. No more ambiguous
    columns errors
    class Post < ApplicationRecord
    belongs_to :author
    has_many :comments
    end
    class Comment < ApplicationRecord
    belongs_to :author
    belongs_to :post
    end
    Post.joins(:comments).group(:author_id).count

    View Slide

  98. New option for
    find_in_batches: finish

    View Slide

  99. New option for
    find_in_batches: finish
    Comment.find_in_batches(start: 1001, finish: 10000, batch_size: 1000) do |group|
    group.each { |comment| do_some_stuff_with_comment(comment) }
    end

    View Slide

  100. OR support in queries

    View Slide

  101. OR support in queries

    View Slide

  102. OR support in queries
    Article.where(author_name: "DHH").or(Article.where(category_name: "Basecamp"))

    View Slide

  103. Acceptance validations
    works properly ;)

    View Slide

  104. Acceptance validations
    works properly ;)
    • Problem: validates_acceptance_of accepts only "1"
    as truthy value

    View Slide

  105. Acceptance validations
    works properly ;)
    • Problem: validates_acceptance_of accepts only "1"
    as truthy value
    • Now it supports both "1" and true

    View Slide

  106. Acceptance validations
    works properly ;)

    View Slide

  107. Acceptance validations
    works properly ;)
    • Bonus: configurable values

    View Slide

  108. Acceptance validations
    works properly ;)
    • Bonus: configurable values
    class Comment < ActiveRecord::Base
    validates_acceptance_of :agreement, accept: [true, "y", "yes", "1", 1, "Rails"]
    end

    View Slide

  109. fields_for accepts lambdas
    for child_index

    View Slide

  110. fields_for accepts lambdas
    for child_index
    • My first Rails contribution ;)

    View Slide

  111. fields_for accepts lambdas
    for child_index
    • My first Rails contribution ;)
    <%= f.simple_fields_for :items, item, child_index: -> { Time.current.to_i } do |builder|
    %>
    <% end %>

    View Slide

  112. fields_for accepts lambdas
    for child_index
    • My first Rails contribution ;)
    <%= f.simple_fields_for :items, item, child_index: -> { Time.current.to_i } do |builder|
    %>
    <% end %>
    ''

    View Slide