Get the primary key field name
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
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
Find a record by conditions, or create a new one if not found (without block)
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
Find a record by conditions, or initialize a new one if not found (without block)
Class methods
Helper for preloading - fetch all records matching a query This is called by the generated preload* methods
Get the average of a column
Example:
User.average(:age)
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.
Count records using a pre-built query builder
Used for counting scoped associations.
Build a query with DISTINCT and block The block receives a Builder and should return the modified Builder
Build a query with DISTINCT on specific columns
Build a query with DISTINCT on specific columns and block The block receives a Builder and should return the modified Builder
Find a record by ID
When an IdentityMap is active, returns the cached instance if available.
Find all records matching a column value
Example:
User.find_all_by("age", 25)
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})
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)
Find a record by a specific column value
Example:
User.find_by("email", "user@example.com")
Find one record matching multiple column conditions
Used primarily for polymorphic associations where we need to match both type and id columns.
Build a query with GROUP BY clause and block The block receives a Builder and should return the modified 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
Get the maximum value of a column
Example:
User.maximum(:age)
Get the minimum value of a column
Example:
User.minimum(:age)
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})
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)
Reset all counter caches for this model to their actual counts
Example:
Publisher.reset_all_counter_caches("books_count", Book, "publisher_id")
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")
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)
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_orderedforfrom_result_set
Example
User.select_list_sql
# => ["\"users\".\"id\" AS \"id\"", "\"users\".\"name\" AS \"name\"", ...]
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
Instance methods
Runtime dynamic getter by string key name This is a method (not macro) that can be called across class boundaries
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
Set preloaded collection (has_many)
Set a preloaded single record (belongs_to, has_one)
Get original value of an attribute before changes
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
Instance method to reset a counter cache
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")
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).
Update attributes and save the record
Example:
user = User.find(1)
user.update(name: "New Name", age: 30)
Macros
Dynamic setter by string key name Handles advanced types (JSON, UUID, Array, Enum) with proper type coercion
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
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
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
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
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:
- Compare ResultSet column names against model's
column_names_ordered - Raise
Ralph::SchemaMismatchErrorif 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
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))