class

Ralph::Model

Inherits Ralph::BulkOperations / Ralph::Associations / Ralph::Callbacks / Ralph::Validations / Reference / Object

Base class for all ORM models

Models should inherit from this class and define their columns using the column macro.

Constructors

create

Create a new record and save it

Source
find_or_create_by(conditions : Hash(String, DB::Any), &block : self -> ) : self

Find a record by conditions, or create a new one if not found

The new record will have the search conditions set as attributes. If a block is given, it will be yielded the new record for additional setup before saving.

Example:

# Without block - creates with just the search conditions
user = User.find_or_create_by({"email" => "alice@example.com"})

# With block for additional attributes
user = User.find_or_create_by({"email" => "alice@example.com"}) do |u|
  u.name = "Alice"
  u.role = "user"
end
Source
find_or_create_by(conditions : Hash(String, DB::Any)) : self

Find a record by conditions, or create a new one if not found (without block)

Source
find_or_initialize_by(conditions : Hash(String, DB::Any), &block : self -> ) : self

Find a record by conditions, or initialize a new one if not found

The new record will have the search conditions set as attributes. If a block is given, it will be yielded the new record for additional setup. The record is NOT saved automatically.

Example:

# Without block
user = User.find_or_initialize_by({"email" => "alice@example.com"})

# With block for additional attributes
user = User.find_or_initialize_by({"email" => "alice@example.com"}) do |u|
  u.name = "Alice"
  u.role = "user"
end
user.save # Must save manually
Source
find_or_initialize_by(conditions : Hash(String, DB::Any)) : self

Find a record by conditions, or initialize a new one if not found (without block)

Source
new

Initialize with attributes

Source

Class methods

_preload_fetch_all(query : Ralph::Query::Builder) : Array(self)

Helper for preloading - fetch all records matching a query This is called by the generated preload* methods

Source
all

Find all records

Source
average(column : String) : Float64 | Nil

Get the average of a column

Example:

User.average(:age)
Source
column_names_ordered

Get column names in the order they should be read from result sets. This matches the order of instance variables in from_result_set. Generated at compile time to ensure consistency.

Source
columns

Get all column metadata

Source
composite_primary_key?

Check if this model has a composite primary key

Source
count

Count all records

Source
count_by(column : String, value) : Int64

Count records matching a column value

Source
count_with_query(query : Ralph::Query::Builder) : Int32

Count records using a pre-built query builder

Used for counting scoped associations.

Source
distinct

Build a query with DISTINCT

Source
distinct

Build a query with DISTINCT and block The block receives a Builder and should return the modified Builder

Source
distinct(*columns : String) : Ralph::Query::Builder

Build a query with DISTINCT on specific columns

Source

Build a query with DISTINCT on specific columns and block The block receives a Builder and should return the modified Builder

Source
find(id)

Find a record by ID

When an IdentityMap is active, returns the cached instance if available.

Source
find_all_by(column : String, value) : Array(self)

Find all records matching a column value

Example:

User.find_all_by("age", 25)
Source
find_all_by_conditions(conditions : Hash(String, DB::Any)) : Array(self)

Find all records matching multiple column conditions

Used primarily for polymorphic associations where we need to match both type and id columns.

Example:

Comment.find_all_by_conditions({"commentable_type" => "Post", "commentable_id" => 1})
Source
find_all_with_query(query : Ralph::Query::Builder) : Array(self)

Find all records using a pre-built query builder

Used primarily for scoped associations where additional WHERE conditions are added to the query via a lambda.

Example:

query = Ralph::Query::Builder.new(User.table_name)
query.where("age > ?", 18)
User.find_all_with_query(query)
Source
find_by(column : String, value) : self | Nil

Find a record by a specific column value

Example:

User.find_by("email", "user@example.com")
Source
find_by_conditions(conditions : Hash(String, DB::Any)) : self | Nil

Find one record matching multiple column conditions

Used primarily for polymorphic associations where we need to match both type and id columns.

Source
first

Find the first record matching conditions

Source
group_by(*columns : String) : Ralph::Query::Builder

Build a query with GROUP BY clause

Source

Build a query with GROUP BY clause and block The block receives a Builder and should return the modified Builder

Source
join_assoc(association_name : Symbol, join_type : Symbol = :inner, alias as_alias : String | Nil = nil) : Ralph::Query::Builder

Join an association by name

This method looks up the association metadata and automatically generates the appropriate join condition.

Example:

User.join_assoc(:posts)              # INNER JOIN posts ON posts.user_id = users.id
Post.join_assoc(:author, :left)      # LEFT JOIN users ON users.id = posts.user_id
User.join_assoc(:posts, :inner, "p") # INNER JOIN posts AS p ON p.user_id = users.id
Source
last

Find the last record

Source
maximum(column : String) : DB::Any | Nil

Get the maximum value of a column

Example:

User.maximum(:age)
Source
minimum(column : String) : DB::Any | Nil

Get the minimum value of a column

Example:

User.minimum(:age)
Source
preload(models : Array(self), associations : Hash(Symbol, T)) : Array(self) forall T
Source
preload(models : Array(self), associations : Symbol) : Array(self)

Preload associations on an existing collection of models

This uses the preloading strategy (separate queries with IN batching). Useful when you already have a collection and want to preload associations.

Example:

authors = Author.all
Author.preload(authors, :posts)
authors.each { |a| a.posts } # Already loaded, no additional queries

# Multiple associations
Author.preload(authors, [:posts, :profile])

# Nested associations
Author.preload(authors, {posts: :comments})
Source
preload(models : Array(self), associations : Array(Symbol)) : Array(self)
Source
primary_key

Get the primary key field name

Source
primary_key_type

Get the primary key type as a string (e.g., "Int64", "UUID", "String")

Source
primary_keys

Get all primary key field names (for composite keys)

Source
query

Get a query builder for this model

Source
query

Find records matching conditions The block receives a Builder and should return the modified Builder (since Builder is immutable, each method returns a new instance)

Source
reset_all_counter_caches(counter_column : String, child_class, foreign_key : String)

Reset all counter caches for this model to their actual counts

Example:

Publisher.reset_all_counter_caches("books_count", Book, "publisher_id")
Source
reset_counter_cache(id, counter_column : String, child_class, foreign_key : String)

Reset a counter cache column to the actual count

This is useful when counter caches get out of sync. Call this on the parent model to reset the counter for a specific record.

Example:

# Reset books_count for publisher with id 1
Publisher.reset_counter_cache(1, "books_count", Book, "publisher_id")

# Or more commonly via instance method
publisher.reset_counter_cache!("books_count", Book, "publisher_id")
Source
scoped

Apply an inline/anonymous scope to a query

This is useful for one-off query customizations that don't need to be defined as named scopes.

The block receives a Builder and should return the modified Builder (since Builder is immutable, each method returns a new instance)

Example:

User.scoped { |q| q.where("active = ?", true).order("name", :asc) }
User.scoped { |q| q.where("age > ?", 18) }.limit(10)
Source
select_list_sql

Get fully-qualified, aliased column expressions for SELECT queries.

Returns columns in the format: "table_name"."column_name" AS "column_name"

This is the safest way to select columns for model hydration because:

  • Explicit table qualification prevents ambiguity in JOINs
  • Explicit aliasing ensures column names in ResultSet match model expectations
  • Column order matches column_names_ordered for from_result_set

Example

User.select_list_sql
# => ["\"users\".\"id\" AS \"id\"", "\"users\".\"name\" AS \"name\"", ...]
Source
sum(column : String) : Float64 | Nil

Get the sum of a column

Example:

User.sum(:age)
Source
table_name

Get the table name for this model

Source
transaction

Execute a block within a transaction

If an exception is raised, the transaction is rolled back. If no exception is raised, the transaction is committed.

Example:

User.transaction do
  user = User.create(name: "Alice")
  Post.create(title: "Hello", user_id: user.id)
end
Source
with_query

Find records matching conditions (alias for query)

Source

Instance methods

_clear_preloaded!

Clear all preloaded associations

Source
_get_attribute(name : String) : DB::Any | Nil

Runtime dynamic getter by string key name This is a method (not macro) that can be called across class boundaries

Source
_get_preloaded_many(association : String) : Array(Model) | Nil

Get preloaded collection

Source
_get_preloaded_one(association : String) : Model | Nil

Get a preloaded single record

Source
_has_preloaded?(association : String) : Bool

Check if an association has been preloaded

Source
_preload_on_class(records : Array(Ralph::Model), assoc : Symbol) : Nil

Instance method to dispatch preloading on this class Used for nested preloading when we have Array(Model) but need to call class-specific preload methods Base implementation - subclasses override this via macro

Source
_set_preloaded_many(association : String, records : Array(Model)) : Nil

Set preloaded collection (has_many)

Source
_set_preloaded_one(association : String, record : Model | Nil) : Nil

Set a preloaded single record (belongs_to, has_one)

Source
changed?(attribute : String) : Bool

Check if a specific attribute has changed

Source
changed?

Check if any attributes have changed

Source
changed_attributes

Get list of changed attributes

Source
changes

Get changes as a hash of attribute => [old, new]

Source
clear_changes_information

Mark all attributes as clean (no changes)

Source
errors

Errors object accessor (using private ivar name to avoid conflicts)

Source
invalid?
Source
new_record?

Check if this is a new record (not persisted)

Source
original_value(attribute : String) : DB::Any | Nil

Get original value of an attribute before changes

Source
persisted?

Check if this record has been persisted to the database Uses explicit @_persisted flag rather than PK presence because non-auto PKs (UUID, String) can be set before the record is saved

Source
reload

Reload the record from the database

Example:

user = User.find(1)
user.reload
Source
reset_counter_cache!(counter_column : String, child_class, foreign_key : String)

Instance method to reset a counter cache

Source
set_attribute(name : String, value : DB::Any) : Nil

Set an attribute by name at runtime

This is useful for dynamic attribute assignment when you have the attribute name as a string.

Example:

user = User.new
user.set_attribute("name", "Alice")
user.set_attribute("email", "alice@example.com")
Source
to_h

Convert model to hash for database operations Handles serialization of advanced types (JSON, UUID, Array, Enum) Uses getter to apply defaults, but catches NilAssertionError for non-nullable columns that don't have a value yet (e.g., auto-increment id).

Source
update

Update attributes and save the record

Example:

user = User.find(1)
user.update(name: "New Name", age: 30)
Source

Macros

__get_by_key_name(name)

Dynamic getter by string key name

Source
__set_by_key_name(name, value)

Dynamic setter by string key name Handles advanced types (JSON, UUID, Array, Enum) with proper type coercion

Source
_generate_preload_dispatcher

Macro to generate dispatch method for preloading associations This is called at compile time to generate a case statement that dispatches to the correct preload<name> method for each association

Source
_generate_preload_on_class

This is a macro that generates a proper typed method in subclasses

Source
after_commit(method_name)

Register an after_commit callback

The callback will be executed after the current transaction commits. If not in a transaction, the callback executes immediately.

Example:

class User < Ralph::Model
  after_commit :send_welcome_email

  def send_welcome_email
    # Send email logic
  end
end
Source
after_rollback(method_name)

Register an after_rollback callback

The callback will be executed if the current transaction is rolled back.

Example:

class User < Ralph::Model
  after_rollback :log_rollback

  def log_rollback
    # Log rollback logic
  end
end
Source
column(decl_or_name, type = nil, primary = false, default = nil)

Define a column on the model

Supports two syntaxes: column id : Int64, primary: true # Type declaration syntax (preferred) column id, Int64, primary: true # Legacy positional syntax

Type declarations control nullability: column name : String # Non-nullable: getter returns String (raises if nil) column bio : String? # Nullable: getter returns String? column age : Int32 | Nil # Nullable: getter returns Int32 | Nil

Options: primary: true - Mark as primary key default: value - Default value for new records

Source
from_result_set(rs)

Create a model instance from a result set

This macro generates code to read columns from a DB::ResultSet and populate a model instance. It includes optional strict validation that checks the ResultSet columns match the model's expected columns.

Schema Validation

When Ralph.settings.strict_resultset_validation is enabled (default: true), the generated code will:

  1. Compare ResultSet column names against model's column_names_ordered
  2. Raise Ralph::SchemaMismatchError if there's any mismatch

This catches issues like:

  • Model missing columns that exist in database
  • Model has extra columns not in database
  • Column order mismatch
  • Schema drift after migrations

Type Mismatch Handling

Each column read is wrapped to catch DB::ColumnTypeMismatchError and re-raise as Ralph::TypeMismatchError with additional context:

  • Which model/column was being read
  • Expected vs actual types
  • Helpful hints for fixing the mismatch
Source
scope(name, block)

Define a named scope for this model

Scopes are reusable query fragments that can be chained together. They're defined as class methods that return Ralph::Query::Builder instances.

The block receives a Ralph::Query::Builder and should return it after applying conditions.

Example without arguments:

class User < Ralph::Model
  table "users"
  column id, Int64, primary: true
  column active, Bool
  column age, Int32

  scope :active, ->(q : Ralph::Query::Builder) { q.where("active = ?", true) }
  scope :adults, ->(q : Ralph::Query::Builder) { q.where("age >= ?", 18) }
end

User.active                    # Returns Builder with active = true
User.active.merge(User.adults) # Chains scopes together
User.active.limit(10)          # Chains with other query methods

Example with arguments:

class User < Ralph::Model
  scope :older_than, ->(q : Ralph::Query::Builder, age : Int32) { q.where("age > ?", age) }
  scope :with_role, ->(q : Ralph::Query::Builder, role : String) { q.where("role = ?", role) }
end

User.older_than(21)
User.with_role("admin").merge(User.older_than(18))
Source
table(name)

Set the table name for this model

Source

Nested types