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

Don't rewrite your framework

Avatar for Vinícius Alonso Vinícius Alonso
April 04, 2025
180

Don't rewrite your framework

Have you ever seen someone rewriting the framework during a code review? After some time studying the Ruby on Rails documentation and doing some research on Reddit, I found some interesting features that are often not known by developers.

This talk was presented at TropicalRails 2025.

Avatar for Vinícius Alonso

Vinícius Alonso

April 04, 2025
Tweet

Transcript

  1. Who am I? • Vinícius Bail Alonso • Sr. Software

    Engineer • Master Degree Student at UTFPR
  2. =

  3. class Customer < ApplicationRecord validates :name, :email, :password, :address_street, :address_city,

    presence: true end class Customer < ApplicationRecord validates :name, :email, :password, presence: true, on: :create validates :address_street, :address_city, presence: true, on: :account_finish end Validations in two di ff erent contexts
  4. customer = Customer.new(name: 'John', email: 'john@google.com', password: '123') customer.valid? #

    => true customer.save # => true customer.valid?(:account_finish) # => false customer.save(context: :account_finish) # => false
  5. class Address attr_reader :street, :city def initialize(street, city) @street =

    street @city = city end def ==(other) @street == other.street && @city == other.city end end
  6. customer = Customer.new customer.address_street = "123 Main St" customer.address_city =

    "Anytown" customer.address #<Address:0x0000ffffb045c6c0 @city="Anytown", @street="123 Main St">
  7. customer = Customer.new customer.address_street = "123 Main St" customer.address_city =

    "Anytown" customer.address #<Address:0x0000ffffb045c6c0 @city="Anytown", @street="123 Main St"> address = Address.new("123 Main St", "Anytown") customer = Customer.new customer.address = address
  8. customer = Customer.new customer.address_street = "123 Main St" customer.address_city =

    "Anytown" customer.address #<Address:0x0000ffffb045c6c0 @city="Anytown", @street="123 Main St"> address = Address.new("123 Main St", "Anytown") customer = Customer.new customer.address = address other_address = Address.new("456 Elm St", "Othertown") customer.address == other_address # false
  9. customer = Customer.new customer.address_street = "123 Main St" customer.address_city =

    "Anytown" customer.address #<Address:0x0000ffffb045c6c0 @city="Anytown", @street="123 Main St"> address = Address.new("123 Main St", "Anytown") customer = Customer.new customer.address = address other_address = Address.new("456 Elm St", "Othertown") customer.address == other_address # false Customer.where(address: Address.new("123 Main St", "Anytown"))
  10. class Cart < ApplicationRecord has_many :items def update_total_price total_price =

    items.map(&:total_price).sum update(total_price: total_price) end end
  11. class Cart < ApplicationRecord has_many :items, after_add: :increment_total_price, after_remove: :decrement_total_price

    def increment_total_price(item) self.total_price = item.total_price + total_price end def decrement_total_price(item) self.total_price = total_price - item.total_price end end
  12. class ReportProcessorJob < ApplicationJob queue_as :default def perform(*args) Customer.all.each do

    |customer| ReportProcessor.new(customer).process end end end 2 problems here: * Full table scan * All customers being loaded in memory at same time
  13. class ReportProcessorJob < ApplicationJob queue_as :default def perform(*args) Customer.find_in_batches do

    |batch| batch.each do |customer| ReportProcessor.new(customer).process end end end end
  14. class ReportProcessorJob < ApplicationJob queue_as :default def perform(*args) Customer.find_in_batches(batch_size: 100)

    do |batch| batch.each do |customer| ReportProcessor.new(customer).process end end end end
  15. Split 200 customers into two workers customers_ids = Customer.ids #

    Worker 1 Customer.find_in_batches(finish: customers_ids[99]) do |batch| batch.each do |customer| ReportProcessor.new(customer).process end end
  16. Split 200 customers into two workers customers_ids = Customer.ids #

    Worker 1 Customer.find_in_batches(finish: customers_ids[99]) do |batch| batch.each do |customer| ReportProcessor.new(customer).process end end # Worker 2 Customer.find_in_batches(start: customers_ids[100]) do |batch| batch.each do |customer| ReportProcessor.new(customer).process end end
  17. c1 = Customer.first c2 = Customer.first c1.address_street = "Fifth Avenue"

    c1.save # => true c2.address_street = "Abbey Road" c2.save # raises ActiveRecord::StaleObjectError
  18. c1 = Customer.first c2 = Customer.first c1.address_street = "Fifth Avenue"

    c1.save # => true c2.reload c2.address_street = "Abbey Road" c2.save # => true
  19. Customer.transaction do customer = Customer.lock.find(3) sleep(10) customer.address_city = "São Paulo"

    customer.save! end Customer.transaction do customer = Customer.lock.find(3) customer.address_city = "Curitiba" customer.save! end Process 1 Process 2
  20. Process 1 Process 2 customers id address_city address_street 1 Curitiba

    Rua XV de Novembro 2 Rio de Janeiro Avenida Atlântica 3 Campinas Avenida Paulista
  21. Process 1 Process 2 customers id address_city address_street 1 Curitiba

    Rua XV de Novembro 2 Rio de Janeiro Avenida Atlântica 3 Campinas Avenida Paulista Locks the row.
  22. Process 1 Process 2 customers id address_city address_street 1 Curitiba

    Rua XV de Novembro 2 Rio de Janeiro Avenida Atlântica 3 Campinas Avenida Paulista Locks the row. Sleeps for 10 seconds
  23. Process 1 Process 2 customers id address_city address_street 1 Curitiba

    Rua XV de Novembro 2 Rio de Janeiro Avenida Atlântica 3 Campinas Avenida Paulista Locks the row. Sleeps for 10 seconds P2 tries to select the row but the row is locked then it waits for P1
  24. Process 1 Process 2 customers id address_city address_street 1 Curitiba

    Rua XV de Novembro 2 Rio de Janeiro Avenida Atlântica 3 São Paulo Avenida Paulista Locks the row Sleeps for 10 seconds P2 tries to select the row but the row is locked then it waits for P1 P1 updates and make row free
  25. Process 1 Process 2 customers id address_city address_street 1 Curitiba

    Rua XV de Novembro 2 Rio de Janeiro Avenida Atlântica 3 Curitiba Avenida Paulista Locks the row Sleeps for 10 seconds P2 tries to select the row but the row is locked then it waits for P1 P1 updates and make row free P2 updates now
  26. class User < ApplicationRecord before_save :sanitize_fields private def sanitize_fields self.email

    = email.strip.downcase if email.present? self.phone = phone.delete("^0-9") if phone.present? end end
  27. class User < ApplicationRecord normalizes :email, with: -> email {

    email.strip.downcase } normalizes :phone, with: -> phone { phone.delete("^0-9") } end
  28. user = User.create(email: " CRUISE-CONTROL@EXAMPLE.COM\n") user.email # => "cruise-control@example.com" https://api.rubyonrails.org/classes/ActiveRecord/Normalization/ClassMethods.html

    User.exists?(email: "\tCRUISE-CONTROL@EXAMPLE.COM ") # => true User.exists?(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]) # => false
  29. user = User.create(email: " CRUISE-CONTROL@EXAMPLE.COM\n") user.email # => "cruise-control@example.com" https://api.rubyonrails.org/classes/ActiveRecord/Normalization/ClassMethods.html

    User.exists?(email: "\tCRUISE-CONTROL@EXAMPLE.COM ") # => true User.exists?(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]) # => false User.normalize_value_for(:phone, "+1 (555) 867-5309") # => "15558675309"
  30. .extending module FilterByRole def only_admin where(role_name: :admin) end def only_manager

    where(role_name: :manager) end end User.extending(FilterByRole).only_admin SELECT "users".* FROM "users" WHERE "users"."role_name" = $1 [["role_name", "admin"]]
  31. .extending module Pagination def page(number) # pagination code goes here

    end end scope = Model.all.extending(Pagination) scope.page(params[:page])
  32. def index ActiveSupport::Notifications.instrument('contacts', extra: :information) do @contacts = Contact.all end

    end ActiveSupport::Notifications.subscribe('contacts') do |event| event.name # => "contacts" event.duration # => 7.537125000031665ms event.payload # => { extra: :information } event.allocations # => 556 (objects) end
  33. ActiveSupport::Notifications.subscribe "process_action.action_controller" do |event| event.name # => "process_action.action_controller" event.duration #

    => 10 (in milliseconds) event.allocations # => 1826 event.payload # => {:extra=>information} Rails.logger.info "#{event} Received!" end
  34. Rails.application.routes.draw do namespace :api do root "home#index" end end class

    API::HomeController < ApplicationController def index render plain: 'API Home' end end
  35. 'posts'.singularize # => "post" 'post'.pluralize # => "posts" 'x-men: the

    last stand'.titleize # => "X Men: The Last Stand" 'employee_salary'.humanize # => "Employee salary"
  36. Rails.application.routes.draw do root "landing#index" namespace :admin do root "home#index" resources

    :contacts, only: [:index, :show, :destroy] end namespace :manager do root "home#index" resources :categories resources :products resources :tables resources :users end end
  37. namespace :admin do root "home#index" resources :contacts, only: [:index, :show,

    :destroy] end namespace :manager do root "home#index" resources :categories resources :products resources :tables resources :users end con fi g/routes/admin.rb con fi g/routes/manager.rb
  38. Rails.application.routes.draw do # ... direct :docs do "https://docs.myapi.com" end direct

    :landing do { controller: 'landing', action: 'index', subdomain: 'www' } end end
  39. The documentation is your best friend Everything in programming involves

    trade-o ff s. Make smart choices In this talk, there are a lot of uncovered features