Share
Embed
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.
Vinícius Alonso
April 04, 2025
Transcript
84 slides
-
Vinícius B. Alonso Don’t rewrite your framework The lesser-known r
a ils fe a tures -
Who am I? • Vinícius Bail Alonso • Sr. Software
Engineer • Master Degree Student at UTFPR -
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 -
customer = Customer.new(name: 'John', email: '[email protected]', password: '123') customer.valid? #
=> true customer.save # => true customer.valid?(:account_finish) # => false customer.save(context: :account_finish) # => false -
customer.address_street = "1105 West Peachtree St" customer.address_city = "Atlanta" customer.valid?(:account_finish)
# => true customer.save(context: :account_finish) # => true -
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 -
class Customer < ApplicationRecord composed_of :address, mapping: { address_street: :street,
address_city: :city } end -
customer = Customer.new customer.address_street = "123 Main St" customer.address_city =
"Anytown" customer.address #<Address:0x0000ffffb045c6c0 @city="Anytown", @street="123 Main St"> -
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 -
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 = 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")) -
class Cart < ApplicationRecord has_many :items end class Item <
ApplicationRecord belongs_to :cart end -
class Cart < ApplicationRecord has_many :items def update_total_price total_price =
items.map(&:total_price).sum update(total_price: total_price) end end -
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 -
class ReportProcessorJob < ApplicationJob queue_as :default def perform(*args) Customer.all.each do
|customer| ReportProcessor.new(customer).process end end end -
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 -
class ReportProcessorJob < ApplicationJob queue_as :default def perform(*args) Customer.find_each do
|customer| ReportProcessor.new(customer).process end end end -
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 -
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 -
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 -
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 -
c1 = Customer.first c2 = Customer.first c1.address_street = "Fifth Avenue"
c1.save # => true c2.address_street = "Abbey Road" c2.save # raises ActiveRecord::StaleObjectError -
c1 = Customer.first c2 = Customer.first c1.address_street = "Fifth Avenue"
c1.save # => true c2.reload c2.address_street = "Abbey Road" c2.save # => true -
Customer.lock.find(1) Customer Load (7.3ms) SELECT "customers".* FROM "customers" WHERE "customers"."id"
= $1 LIMIT $2 FOR UPDATE [["id", 1], ["LIMIT", 1]] -
Customer.lock.find(1) Customer Load (7.3ms) SELECT "customers".* FROM "customers" WHERE "customers"."id"
= $1 LIMIT $2 FOR UPDATE [["id", 1], ["LIMIT", 1]] Row-level lock -
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 -
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 -
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. -
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 -
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 -
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 -
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 -
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 -
class User < ApplicationRecord normalizes :email, with: -> email {
email.strip.downcase } normalizes :phone, with: -> phone { phone.delete("^0-9") } end -
user = User.create(email: " [email protected]\n") user.email # => "[email protected]" https://api.rubyonrails.org/classes/ActiveRecord/Normalization/ClassMethods.html
User.exists?(email: "\[email protected] ") # => true User.exists?(["email = ?", "\[email protected] "]) # => false -
user = User.create(email: " [email protected]\n") user.email # => "[email protected]" https://api.rubyonrails.org/classes/ActiveRecord/Normalization/ClassMethods.html
User.exists?(email: "\[email protected] ") # => true User.exists?(["email = ?", "\[email protected] "]) # => false User.normalize_value_for(:phone, "+1 (555) 867-5309") # => "15558675309" -
.extending module FilterByRole def only_admin where(role_name: :admin) end def only_manager
where(role_name: :manager) end end -
.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"]] -
.extending module Pagination def page(number) # pagination code goes here
end end scope = Model.all.extending(Pagination) scope.page(params[:page]) -
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 -
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 -
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 -
'posts'.singularize # => "post" 'post'.pluralize # => "posts" 'x-men: the
last stand'.titleize # => "X Men: The Last Stand" 'employee_salary'.humanize # => "Employee salary" -
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 -
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 -
Rails.application.routes.draw do # ... direct :docs do "https://docs.myapi.com" end direct
:landing do { controller: 'landing', action: 'index', subdomain: 'www' } end end -
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