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

What's New in Rails 5?

karol.galanciak
September 28, 2016
200

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 full-size slide

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

    View full-size slide

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

    View full-size slide

  4. ActionCable - server-side

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  7. 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 full-size slide

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

    View full-size slide

  9. 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 full-size slide

  10. ActionCable - client-side

    View full-size slide

  11. 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 full-size slide

  12. Rails API
    • rails new rails_5_app --api

    View full-size slide

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

    View full-size slide

  14. Attributes API

    View full-size slide

  15. 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 full-size slide

  16. 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 full-size slide

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

    View full-size slide

  18. 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 full-size slide

  19. 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 full-size slide

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

    View full-size slide

  21. 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 full-size slide

  22. Assets silenced by default in
    development

    View full-size slide

  23. Assets silenced by default in
    development
    • quite_assets gem

    View full-size slide

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

    View full-size slide

  25. 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 full-size slide

  26. ApplicationRecord

    View full-size slide

  27. ApplicationRecord
    • New base class for models

    View full-size slide

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

    View full-size slide

  29. Rendering outside controllers

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  32. Rake / Rails commands

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  35. Caching collections

    View full-size slide

  36. Caching collections
    • ActiveRecord::Collection#cache_key

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  40. 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 full-size slide

  41. 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 full-size slide

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

    View full-size slide

  43. Belongs-to association
    required by default

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  47. redirect_back method

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  50. ActiveRecord::Base.Suppress

    View full-size slide

  51. ActiveRecord::Base.Suppress

    View full-size slide

  52. 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 full-size slide

  53. 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 full-size slide

  54. Left Outer Join Support

    View full-size slide

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

    View full-size slide

  56. Config for UUIDs as primary keys

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  59. Queue adapter configurable
    per job

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  62. ApplicationJob base class

    View full-size slide

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

    View full-size slide

  64. Validation of multiple contexts

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  68. Additional details for
    validations

    View full-size slide

  69. 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 full-size slide

  70. Saving records without
    timestamp update

    View full-size slide

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

    View full-size slide

  72. Bidirectional destroy

    View full-size slide

  73. Bidirectional destroy
    • Doesn’t cause SystemStackError

    View full-size slide

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

    View full-size slide

  75. ActiveModel::AttributesAssi
    gnment

    View full-size slide

  76. 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 full-size slide

  77. ActionController::Parameters is
    not a HashWithIndifferentAccess

    View full-size slide

  78. 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 full-size slide

  79. 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 full-size slide

  80. Comments in migrations

    View full-size slide

  81. 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 full-size slide

  82. 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 full-size slide

  83. Updating collection with
    callbacks and validations

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  86. 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 full-size slide

  87. Expression indexes

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  90. Indexed errors for nested
    attributes

    View full-size slide

  91. 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 full-size slide

  92. 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 full-size slide

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

    View full-size slide

  94. No more ambiguous
    columns errors

    View full-size slide

  95. 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 full-size slide

  96. New option for
    find_in_batches: finish

    View full-size slide

  97. 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 full-size slide

  98. OR support in queries

    View full-size slide

  99. OR support in queries

    View full-size slide

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

    View full-size slide

  101. Acceptance validations
    works properly ;)

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  104. Acceptance validations
    works properly ;)

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  107. fields_for accepts lambdas
    for child_index

    View full-size slide

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

    View full-size slide

  109. 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 full-size slide

  110. 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 full-size slide