What's New in Rails 5?

347a1b64f3a6e38981bc99b53919e2b1?s=47 karol.galanciak
September 28, 2016
160

What's New in Rails 5?

347a1b64f3a6e38981bc99b53919e2b1?s=128

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
  2. ActionCable

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

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

    application • Both client-side and server-side tools
  5. ActionCable - server-side

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

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

    the sever • Authentication / authorization goes here
  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
  9. ActionCable - server-side • Channels - endpoints where the actual

    logic happens (~ controllers)
  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
  11. ActionCable - client-side

  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("<p>" + data["sent_by"] + ": " + data["body"] + "</p>") } }); App.chatChannel.send({ sent_by: "DHH", body: "OMG, it works!" });
  13. Rails API

  14. Rails API • rails new rails_5_app --api

  15. Rails API • rails new rails_5_app —api • ApplicationController inherits

    from ActionController::API • Modifies the generators (no views, helpers and assets)
  16. Attributes API

  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)
  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"]
  19. Attributes API • custom types (requires #deserialize method for assigning

    values and #serialize when querying database)
  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
  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
  22. Class / module variables per thread • cattr_accessor / mattr_accessor

    - but per thread - thread_cattr_accessor / thread_mattr_accessor
  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
  24. Assets silenced by default in development

  25. Assets silenced by default in development • quite_assets gem

  26. Assets silenced by default in development • quite_assets gem •

    No more garbage in logs
  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
  28. ApplicationRecord

  29. ApplicationRecord • New base class for models

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

    has_paper_trail end class User < ApplicationRecord end
  31. Rendering outside controllers

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

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

    assigns: { order: Order.find(order_id) })
  34. Rake / Rails commands

  35. Rake / Rails commands • All rails commands can be

    executed with rake and all rake commands can be executed with rails
  36. Rake / Rails commands rails db:migrate rake routes | grep

    some_route -> rails routes -g some_route
  37. Caching collections

  38. Caching collections • ActiveRecord::Collection#cache_key

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

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

    %> <p> <%= render post %> </p> <% end %> <% end %>
  41. ActiveRecord no longer halts execution of the callbacks when returning

    false
  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
  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
  44. ActiveRecord no longer halts execution of the callbacks when returning

    false • configurable with ActiveSupport.halt_callback_chains_on_return_false
  45. Belongs-to association required by default

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

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

    :author, optional: true end
  48. Belongs-to association required by default • Configurable with Rails.application.config.active_record.belongs_to_r equired_by_default

  49. redirect_back method

  50. redirect_back method class UsersController < ApplicationController def update user =

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

    find_user user.update(params[:user]) redirect_back(fallback_location: root_path) end end
  52. ActiveRecord::Base.Suppress

  53. ActiveRecord::Base.Suppress

  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
  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
  56. Left Outer Join Support

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

  58. Config for UUIDs as primary keys

  59. Config for UUIDs as primary keys create_table :orders, id: :uuid

    do |t| end
  60. Config for UUIDs as primary keys • Configurable with: config.active_record.primary_key

    = :uuid
  61. Queue adapter configurable per job

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

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

    = :sidekiq end
  64. ApplicationJob base class

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

  66. Validation of multiple contexts

  67. Validation of multiple contexts class User < ActiveRecord::Base validate :validate_confirmation_info,

    on: :confirmation validate :validate_updatable, on: :update end
  68. Validation of multiple contexts • Before: user.valid?(:confirmation) && user.valid?(:update)

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

  70. Additional details for validations

  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}]}
  72. Saving records without timestamp update

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

  74. Bidirectional destroy

  75. Bidirectional destroy • Doesn’t cause SystemStackError

  76. Bidirectional destroy class Account < ActiveRecord::Base has_one :billing_address, dependent: :destroy

    end class BillingAddress < ActiveRecord::Base belongs_to :account, dependent: :destroy end
  77. ActiveModel::AttributesAssi gnment

  78. ActiveModel::AttributesAssi gnment class UserForm include ActiveModel::AttributeAssignment attr_accessor :email, :password, :password_confirmation

    end form = UserForm.new form.assign_attributes(email: "email@example.com")
  79. ActionController::Parameters is not a HashWithIndifferentAccess

  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
  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
  82. Comments in migrations

  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
  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
  85. Updating collection with callbacks and validations

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

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

    validations and callbacks • But new ActiveRecord::Relation.update does ;)
  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: "")
  89. Expression indexes

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

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

  92. Indexed errors for nested attributes

  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"]}
  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"]}
  95. Indexed errors for nested attributes • Configurable with config.active_record.index_nested_attribute_errors

  96. No more ambiguous columns errors

  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
  98. New option for find_in_batches: finish

  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
  100. OR support in queries

  101. OR support in queries

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

  103. Acceptance validations works properly ;)

  104. Acceptance validations works properly ;) • Problem: validates_acceptance_of accepts only

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

    "1" as truthy value • Now it supports both "1" and true
  106. Acceptance validations works properly ;)

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

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

    Comment < ActiveRecord::Base validates_acceptance_of :agreement, accept: [true, "y", "yes", "1", 1, "Rails"] end
  109. fields_for accepts lambdas for child_index

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

    ;)
  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 %>
  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 %> '<input id="invoice_items_attributes_abc_description" name="invoice[items_attributes] [1475065682][description]" type="text" value="" />'