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 needed in the past.

# ActiveRecord

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.

  • 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")
    

# 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

  • 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.
    • Just add an integer column named lock_version to a given table, with a default value of zero.
    • Handle 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

# QueryingOther

  • Where
    • JSON field (Postgres)
      User.where('preferences @> ?', { newsletter: true }.to_json)
      
  • Bind Variables
    Message.where("subject LIKE :foo OR body LIKE :foo", foo: '%woah%')
    
  • With Joins
    User.joins(:posts).where(posts: { active: true })
    
  • Limit/Offset

    Aliased as take/skip

    Timesheet.take(10).skip(10)
    
  • Select
    • 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

    LEFT OUTER JOIN to grab additional associated data. Delegates to eager_load when joining, and to preload when doing two queries (an additional WHERE IN)to get the associated data.With where

    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)
    
    • find_by_sql(String)
      • Returns 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)

# 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.
    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")
    

# 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) }
  • Extenting 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

Search Results