Alex's Slip-box

These are my org-mode notes in sort of Zettelkasten style

ActiveRecord

:ID: 40FFCDB2-F065-4EDC-9DED-C3007827B470

This is a big ass dump of ActiveRecod stuff I’ve learned.

See also:

An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data. —Martin Fowler, Patterns of Enterprise Architecture

# ActiveRecord Configuration

# Override Table Name and Primary Key

  • Useful for when you building data models for a database you don’t control.
class Client < ApplicationRecord
  self.table_name = "CLIENT"
  self.primary_key = "CID"
end

# Pluralization

Rails.application.config.active_record.pluralize_table_names = false

# Other Customizations

Attributes of ActiveRecord::Base

primary_key_prefix
table_name_prefix
table_name_suffix

# Override and Customize Accessor

  • write_attribute
  • read_attribute
def message=(txt)
  write_attribute(:message, txt + ' in bed')
end
#  or
def message=(txt)
  self[:message] = txt + ' in bed'
end

# Query Cache

By default, Rails attempts to optimize performance by turning on a simple query cache. It is a hash stored on the current thread, one for every active database connection.

The cache lives for the duration of a controller action.

See also http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/QueryCache.html

# Manual Query Cache

User.cache do
  puts User.first
  puts User.first
  puts User.first
end

# Attributes

# Hash Notation

  • AR objects can often be treated like a hash.
  • Can access attrs with bracket notation or call slice directly on the object.
user[:name]
user.slice(:name)
user.attributes[:name]

# Typecasting

*attribute*_before_type_cast_method

before_validation :fix_rate
def fix_rate
  self[:rate] = rate_before_type_cast.tr('$,','')
end

# Readonly

  • Set an attribute to be read only.
  • Doesn’t apply to new records getting saved for the first time.
  • Doesn’t raise errors when attempting to save a read only attribute.
attr_readonly :social_security_number

# has_secure_token

  • Generate a token.
  • Can assign a column name by passing a symbol representing the column name (defaults to :token)
  • If you declare the type of your column as token in a migration, then Rails will automatically add it as a string with a unique index.
class User < ApplicationRecord
  has_secure_token
end
>> user = User.create
>> => # < User id: … >> user.token = > “njHcvhKSwX9toZKEe9YETA8C”>> user.regenerate_token
has_secure_token :auth_token

# Attributes API

attribute :category, :string, default: 'n/a'
attribute :repeats, :boolean, default: false

# Custom Types

ActiveRecord::Type.register :inquiry, Inquiry.new
class Inquiry < ActiveRecord::Type::String
  def type
    :inquiry
  end

  def cast(value)
    super.inquiry #  See docs for [String.inquiry](:note:059eadc0-9cd8-4955-bb41-b579e5877cb7)
  end
end

class Event < ApplicationRecord
  attribute :repeats, Inquiry.new
end

# Counting Associations

User.joins(:comments).group("users.name").count("comments.id")

# Counter cache

  • Depends on a ~*_count column added to the parent table.
  • declared counter_cache adds callbacks to update the count cache on the parent
belongs_to :user, counter_cache: true

See also https://api.rubyonrails.org/classes/ActiveRecord/CounterCache/ClassMethods.html

# Store

Wrap key/value store columns (eg, jsonb) and define accessors. Add typecasting wit https://github.com/palkan/store_attribute

module Asteroid
  class NearEarthObject < ApplicationRecord
    validates :name, :absolute_magnitude_h, :estimated_diameter, presence: true
    store :estimated_diameter, accessors: %i[kilometers meters miles feet]
    alias_attribute :id, :neo_reference_id
  end
end

# Update

# Mass Updates

update_all

  • 2nd arg is an optional where clause. The args can be SQL strings as well.
Project.update_all({manager: 'Ron Campbell'}, technology: 'Rails')
user.post_ids = [1, 2, 3]

# Mass Delete

Timesheet.delete(1)
Timesheet.destroy([2, 3])
delete_all # skips callbacks (like dependent: :destroy)
destroy_all

# Skipping Validation or Callbacks

update_attribute: Update a single attribute and skip validation (still runs callbacks). update_column / update_columns: skips validation and callbacks touch: false: Skips setting the timestamps

user.save(touch: false)

validate: false

# Updating Timestamps (touch)

user.touch # => sets updated_at to now. user.touch(:viewed_at) #  sets viewed_at and updated_at to now.

# Timestamps via association

class User < ApplicationRecord
  belongs_to :client, touch: true
end
user.touch #  also calls user.client.touch

# Database Locking

See also Database Locks

# Optimistic Locking

Good for infrequent collisions. Database records are not actually locked. If two different model instances are loaded for the same record and saved differently, the first one wins while the second one raises ActiveRecord::StaleObject.

  • Add an integer column named lock_version to a given table, with a default value of zero.
  • Where the records are updated, add handlers for the ActiveRecord::StaleObject error

# Pessimistic Locking

Locks the records returns from a select statement. Works with transactions. Records are released when the transaction completes. Keep the transactions small to make sure they execute quickly.

Timesheet.transaction do
  t = Timesheet.lock.first
  t.approved = true
  t.save!
end

Can also pass a locking clause

Foo.active.lock("FOR UPDATE OF pa_requests NOWAIT")

# QueryingOther

# Where with JSON field (Postgres)

User.where('preferences @> ?', { newsletter: true }.to_json)

# Bind Variables

Message.where("subject LIKE :foo OR body LIKE :foo", foo: '%woah%')

# Limit/Offset

Aliased as take/skip

Timesheet.take(10).skip(10)

# Select

Using select for a limited set of columns could increase query performance by making it more likely the data will be queried using an efficient Index Only Scan.

  • Add calculated or composed columns
  • NOTE: the calculated column will show in attributes method or calling the method directly
b = BillableWeek.select('*', "mon_hrs + tues_hrs as two_day_total").first
b.two_day_total # => 16

# From (and Aliasing Table Names)

Topic.select('title').from(Topic.approved).to_sql
# => "SELECT title FROM (SELECT * FROM topics WHERE approved = 't')"

# Ignoring columns

class User < ApplicationRecord
  self.ignored_columns = %w(some_stupid_bs_column)
end

# Group

Usually used with select.

users = Account.select('name, SUM(cash) as money').group('name').to_a

# With having(clauses).

Examples…

Dealer.joins(:quotes).group("dealers.id").having('count(quotes.id) > 1')
User.group("created_at").having(["created_at > ?", 2.days.ago])
Person.having('min(age) > 17').group(:last_name).minimum(:age)

# Includes / Eager Loading / Preloading

Using includes will produce LEFT OUTER JOIN to grab additional associated data if there is a where condition. Otherwise will do two queries (an additional WHERE IN)to get the associated data.

Using strict_loading will raise an error on attempted lazy loading. In order to get access to associated records, one must eager load.

User.includes(:auctions).where(auctions: { name: 'Lumina' })

# find_or_create_by / create_with

Use retry in a begin / end block when rescuing ActiveRecord::RecordNotUnique to deal with race conditions.

User.create_with(active: true).find_or_create_by(first_name: 'Buster', ...)
User.find_or_create_by(first_name: 'Poppa') do |user|
  user.last_name = 'Smurf'
end

# Explain

Asteroid::NearEarthObject.select("a.name").from(Asteroid::NearEarthObject.where(is_potentially_hazardous_asteroid: false), :a).explain

# Existence

  • exists?
  • any?
  • empty? (returns the count if not empty or true)
  • many? (more than one)
  • one?
  • None The query method returns Active-Record::NullRelation, which is an implementation of the Null Object pattern. It is to be used in instances where you have a method that returns a relation, but there is a condition in which you do not want the database to be queried. All subsequent chained conditions will work without issue, eliminating the need to continuously check whether the object your are working with is a relation.

# Other Noteworthy Query Methods

  • readonly
  • reorder
  • reverse_order
  • merge
  • only(*onlies)
  • except(*skips)
  • to_json (also to_xml and to_yaml)
  • unscope (useful for unscoping default scopes). Can take :from, :having, :joins, …etc
Member.where(name: "Fool", active: true).unscope(where: :name)
Member.unscope(:active)...
  • unscoped (removes all scopes including the default scope)
Timesheet.unscoped.where("created_at < ?", 1.year.ago)

# Query using SQL

# find_by_sql(String)

User.find_by_sql("select * from users limit 1")
  • Returns AR objects based on the SQL string arg
  • Use bind variables
  • Use this for writing complex SQL queries that don’t lend themselves well to using AR’s query API (like having to use CTEs)

# execute

  • This returns a PG::Result object.
  • This can save a lot of memory since it will not return AR objects.
ActiveRecord::Base.connection.execute

# Using the Database Connection Directly

  • Using a connection object.
  • There’s lots of methods that can be called on the connect adapter object.
ActiveRecord::Base.connectionconn.tables #  => an array of the table names
ActiveRecord::Base.connection.execute("select * from asteroid_near_earth_objects").values
  • .select_rows(“select * from asteroid_orbit”)
  • .select_all
  • .select_one
  • .select_values

# Executing a SQL script in a file

  • Read the file, split on ‘;’ and execute each statement.
def execute_sql_file(path)
  File.read(path). split(';').each do |sql|
    begin
      ActiveRecord::Base.connection.execute(#{sql}") unless sql.blank?
    rescue ActiveRecord::StatementInvalid
        $stderr.puts "warning: #{$!}"
    end
  end
end

# Polymorphic Associations

# Doing SQL joins with polymorphic associations.

# SQL join strings

Activity.joins("LEFT JOIN users ON activities.owner_type = 'User' AND activities.owner_id = users.id")
Activity.joins("LEFT JOIN managers ON activities.owner_type = 'Manager' AND activities.owner_id = managers.id")

# Self referential join

This does a self referential join including the record_type as part of the join clause. Then joins to the record through it. Then following is from an ActiveStorage::Attachment extension in https://github.com/apmiller108/tmp/tree/main/app/models/concerns

belongs_to :record, polymorphic: true
has_one :self_ref, class_name: 'ActiveStorage::Attachment', foreign_key: :id
has_one :rich_text, through: :self_ref, source: :record, source_type: 'ActionText::RichText'
SELECT "active_storage_attachments".* FROM "active_storage_attachments"
INNER JOIN "active_storage_attachments" "self_refs_active_storage_attachments_join"
    ON "self_refs_active_storage_attachments_join"."record_type" = 'ActionText::RichText'
    AND "self_refs_active_storage_attachments_join"."id" = "active_storage_attachments"."id"
INNER JOIN "action_text_rich_texts"
    ON "action_text_rich_texts"."id" = "self_refs_active_storage_attachments_join"."record_id"

# Associations

# has_many

  • returns ActiveRecord::CollectionProxy
    • @owner
    • @target
    • @reflection

# ActiveRecord::CollectionProxy Methods

Here’s a bunch of examples…

user.timesheets.select(:*, "calc_something(col1, col2) as delta").to_a
user.timesheets.closed.each(&:mark_for_destruction)
  • before_add, after_add
has_many :unchangable_posts, class_name: "Post", before_add: ->(owner, record) { raise "Can't do it!" }
has_many :pending_comments, -> { where( approved: true) }, class_name: 'Comment'
class Client < ActiveRecord::Base
  has_many :timesheets, -> { distinct }, through: :billable_weeks
end
class User < ActiveRecord::Base
  has_many :timesheets
  has_one :latest_sheet, -> { order(' created_at desc') }, class_name: 'Timesheet'
end
belongs_to :project, -> { readonly }
belongs_to :post, -> { includes(:author) }

# Extending Associations

Methods…

has_many :people do
  def named(full_name)
    first_name, last_name = full_name.split(" ", 2)
    where(first_name: first_name, last_name: last_name).first_or_create
  end
end

Same thing, but using modules…

has_many :people, -> { extending(ByNameExtension, ByRecentExtension) }

# Association with null object pattern

belongs_to :automatic_payment_method, class_name: 'PaymentMethod'

def automatic_payment_method
  super || NullAutomaticPaymentMethod.new
end

# Callback Classes

It is common enough to want to reuse callback code for more than one object that Rails gives you a way to write callback classes. All you have to do is: Pass a given callback queue an object that responds to the name of the callback and takes the model object as a parameter.

Soft delete example

class MarkDeleted
  def self.before_destroy(model)
    model.update_attribute(:deleted_at, Time.current)
    throw(:abort)
  end
end

class Account < ActiveRecord::Base
  before_destroy MarkDeleted
end

# Value Objects

Unlike with object identity, value objects are considered equal when their attributes are equal. Example of an Address type value object

class Person < ActiveRecord::Base
  def address
    @address || = Address.new(address_city, address_state)
  end

  def address=(address)
    self[:address_city] = address.city
    self[:address_state] = address.state
    @address = address
  end
end

class Address
  attr_reader :city, :state

  def initialize(city, state)
    @city, @state = city, state
  end

  def ==(other_address)
    city == other_address.city && state == other_address.state
  end
end

# Gems / Extensions

See also Ruby gems

# store_attribute

# Query logs

See also https://api.rubyonrails.org/classes/ActiveRecord/QueryLogs.html

Append or prepend comments to queries so you can find them in logs easier.

/*application:MyApp,controller:things,action:index*/​

# Schema Format

Change the format to from ruby (db/schema.rb) to SQL (db/structure.sql) with:

# config/application.rb
config.active_record.schema_format = :sql

This uses pg_dump. Pass flags to PG dump as needed. For available options run pg_dump --help See also https://github.com/rails/rails/issues/38695

module Hubble
  class Application < Rails::Application
    config.active_record.schema_format = :sql
    ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = ['--exclude-schema=rollback', '--if-exists', '--clean']
    ...
  end
end

Search Results