ActiveRecord
Table of Contents
- ActiveRecord Configuration
- Override Table Name and Primary Key
- Pluralization
- Other Customizations
- Override and Customize Accessor
- Query Cache
- Attributes
- Store
- Update
- Database Locking
- QueryingOther
- Query using SQL
- Using the Database Connection Directly
- Executing a SQL script in a file
- Polymorphic Associations
- Associations
- ActiveRecord::CollectionProxy Methods
- Callback Classes
- Value Objects
- Gems / Extensions
- Query logs
- Schema Format
: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
- Gives you is the capability to declaratively tell Active Record how to do type coercion.
- Available Types: [[https://github.com/rails/rails/tree/master/activemodel/lib/active_model/type]]
- Options
:array
(boolean for postgres array field):range
(provide range object for postgres range type):default
As opposed to setting a default value on the database level.
attribute :category, :string, default: 'n/a' attribute :repeats, :boolean, default: false
# Custom Types
- [[https://blog.metova.com/rails-5-attributes-api]]
- Register Type
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)
- Useful for referencing subqueries or views.
- See also:
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
(alsoto_xml
andto_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