Slide 1

Slide 1 text

Vinícius B. Alonso Don’t rewrite your framework The lesser-known r a ils fe a tures

Slide 2

Slide 2 text

Who am I? • Vinícius Bail Alonso • Sr. Software Engineer • Master Degree Student at UTFPR

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

=

Slide 5

Slide 5 text

api.rubyonrails.org + reddit.com =

Slide 6

Slide 6 text

Context validations

Slide 7

Slide 7 text

class Customer < ApplicationRecord validates :name, :email, :password, :address_street, :address_city, presence: true end

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

customer.address_street = "1105 West Peachtree St" customer.address_city = "Atlanta" customer.valid?(:account_finish) # => true customer.save(context: :account_finish) # => true

Slide 11

Slide 11 text

Aggregations

Slide 12

Slide 12 text

class Customer < ApplicationRecord validates :name, :email, :password, :address_street, :address_city, presence: true end

Slide 13

Slide 13 text

https://martinfowler.com/bliki/ValueObject.html

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

class Customer < ApplicationRecord composed_of :address, mapping: { address_street: :street, address_city: :city } end

Slide 16

Slide 16 text

customer = Customer.new customer.address_street = "123 Main St" customer.address_city = "Anytown" customer.address #

Slide 17

Slide 17 text

customer = Customer.new customer.address_street = "123 Main St" customer.address_city = "Anytown" customer.address # address = Address.new("123 Main St", "Anytown") customer = Customer.new customer.address = address

Slide 18

Slide 18 text

customer = Customer.new customer.address_street = "123 Main St" customer.address_city = "Anytown" customer.address # 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

Slide 19

Slide 19 text

customer = Customer.new customer.address_street = "123 Main St" customer.address_city = "Anytown" customer.address # 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"))

Slide 20

Slide 20 text

Association Callbacks

Slide 21

Slide 21 text

class Cart < ApplicationRecord has_many :items end class Item < ApplicationRecord belongs_to :cart end

Slide 22

Slide 22 text

class Cart < ApplicationRecord has_many :items def update_total_price total_price = items.map(&:total_price).sum update(total_price: total_price) end end

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

cart.items << item cart.update_total_price cart.items << item cart.save Before After

Slide 25

Slide 25 text

Batches

Slide 26

Slide 26 text

class ReportProcessorJob < ApplicationJob queue_as :default def perform(*args) Customer.all.each do |customer| ReportProcessor.new(customer).process end end end

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

class ReportProcessorJob < ApplicationJob queue_as :default def perform(*args) Customer.find_each do |customer| ReportProcessor.new(customer).process end end end

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Split 200 customers into two workers customers_ids = Customer.ids

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

Optimistic Locking

Slide 35

Slide 35 text

class AddLockVersionToCustomers < ActiveRecord::Migration[7.0] def change add_column :customers, :lock_version, :integer end end

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

c1 = Customer.first c2 = Customer.first c1.address_street = "Fifth Avenue" c1.save # => true c2.reload c2.address_street = "Abbey Road" c2.save # => true

Slide 38

Slide 38 text

Pessimistic Locking

Slide 39

Slide 39 text

Customer.lock.find(1)

Slide 40

Slide 40 text

Customer.lock.find(1) Customer Load (7.3ms) SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 LIMIT $2 FOR UPDATE [["id", 1], ["LIMIT", 1]]

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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.

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Normalize

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

class User < ApplicationRecord normalizes :email, with: -> email { email.strip.downcase } normalizes :phone, with: -> phone { phone.delete("^0-9") } end

Slide 52

Slide 52 text

user = User.create(email: " [email protected]\n") user.email # => "[email protected]" https://api.rubyonrails.org/classes/ActiveRecord/Normalization/ClassMethods.html

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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"

Slide 55

Slide 55 text

Query methods

Slide 56

Slide 56 text

.extending module FilterByRole def only_admin where(role_name: :admin) end def only_manager where(role_name: :manager) end end

Slide 57

Slide 57 text

.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"]]

Slide 58

Slide 58 text

.extending module Pagination def page(number) # pagination code goes here end end scope = Model.all.extending(Pagination) scope.page(params[:page])

Slide 59

Slide 59 text

Instrumentation

Slide 60

Slide 60 text

def index @contacts = Contact.all end

Slide 61

Slide 61 text

def index ActiveSupport::Notifications.instrument('contacts', extra: :information) do @contacts = Contact.all end end

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

No content

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

String Inquirer

Slide 66

Slide 66 text

Rails.env.production?

Slide 67

Slide 67 text

Rails.env.production? env = ActiveSupport::StringInquirer.new(‘production') env.production? # => true env.test? # => false

Slide 68

Slide 68 text

In fl ector

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

No content

Slide 71

Slide 71 text

ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym "API" end con fi g/initializers/in fl ections.rb

Slide 72

Slide 72 text

'posts'.singularize # => "post" 'post'.pluralize # => "posts" 'x-men: the last stand'.titleize # => "X Men: The Last Stand" 'employee_salary'.humanize # => "Employee salary"

Slide 73

Slide 73 text

Draw

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

Rails.application.routes.draw do root "landing#index" draw :admin draw :manager end con fi g/routes.rb

Slide 77

Slide 77 text

Direct routes

Slide 78

Slide 78 text

Rails.application.routes.draw do # ... direct :docs do "https://docs.myapi.com" end direct :landing do { controller: 'landing', action: 'index', subdomain: 'www' } end end

Slide 79

Slide 79 text

rails console app.docs_url => "https://docs.myapi.com" app.landing_url => "http://www.example.com/"

Slide 80

Slide 80 text

Conclusion

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

Thank you!

Slide 83

Slide 83 text

Get in touch [email protected] @alonsoemacao viniciusalonso

Slide 84

Slide 84 text

https://www.viniciusalonso.com/2025/04/01/tropicalrails-references.html References