Model¶
class
Defined in src/ralph/model.cr:19
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
.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" => "[email protected]"})
# With block for additional attributes
user = User.find_or_create_by({"email" => "[email protected]"}) do |u|
u.name = "Alice"
u.role = "user"
end
.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)
.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" => "[email protected]"})
# With block for additional attributes
user = User.find_or_initialize_by({"email" => "[email protected]"}) do |u|
u.name = "Alice"
u.role = "user"
end
user.save # Must save manually
.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)
.new¶
Initialize with attributes
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
.all¶
Find all records
.average(column : String) : Float64 | Nil¶
Get the average of a column
Example:
.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.
.columns¶
Get all column metadata
.count¶
Count all records
.count_by(column : String, value) : Int64¶
Count records matching a column value
.count_with_query(query : Ralph::Query::Builder) : Int32¶
Count records using a pre-built query builder
Used for counting scoped associations.
.distinct¶
Build a query with DISTINCT
.distinct¶
Build a query with DISTINCT and block The block receives a Builder and should return the modified Builder
.distinct(*columns : String) : Ralph::Query::Builder¶
Build a query with DISTINCT on specific columns
.distinct(*columns : String, &block : Ralph::Query::Builder -> Ralph::Query::Builder) : Ralph::Query::Builder¶
Build a query with DISTINCT on specific columns and block The block receives a Builder and should return the modified Builder
.find(id)¶
Find a record by ID
When an IdentityMap is active, returns the cached instance if available.
.find_all_by(column : String, value) : Array(self)¶
Find all records matching a column value
Example:
.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:
.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)
.find_by(column : String, value) : self | Nil¶
Find a record by a specific column value
Example:
User.find_by("email", "[email protected]")
.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.
.first¶
Find the first record matching conditions
.group_by(*columns : String) : Ralph::Query::Builder¶
Build a query with GROUP BY clause
.group_by(*columns : String, &block : Ralph::Query::Builder -> Ralph::Query::Builder) : Ralph::Query::Builder¶
Build a query with GROUP BY clause and block The block receives a Builder and should return the modified Builder
.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
.last¶
Find the last record
.maximum(column : String) : DB::Any | Nil¶
Get the maximum value of a column
Example:
.minimum(column : String) : DB::Any | Nil¶
Get the minimum value of a column
Example:
.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})
.primary_key¶
Get the primary key field name
.primary_key_type¶
Get the primary key type as a string (e.g., "Int64", "UUID", "String")
.query¶
Get a query builder for this model
.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)
.reset_all_counter_caches(counter_column : String, child_class, foreign_key : String)¶
Reset all counter caches for this model to their actual counts
Example:
.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")
.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)
.sum(column : String) : Float64 | Nil¶
Get the sum of a column
Example:
.table_name¶
Get the table name for this model
.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
.with_query¶
Find records matching conditions (alias for query)
Instance Methods¶
#_clear_preloaded!¶
Clear all preloaded associations
#_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
#_get_preloaded_many(association : String) : Array(Model) | Nil¶
Get preloaded collection
#_get_preloaded_one(association : String) : Model | Nil¶
Get a preloaded single record
#_has_preloaded?(association : String) : Bool¶
Check if an association has been preloaded
#_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
#_set_preloaded_many(association : String, records : Array(Model)) : Nil¶
Set preloaded collection (has_many)
#_set_preloaded_one(association : String, record : Model | Nil) : Nil¶
Set a preloaded single record (belongs_to, has_one)
#changed?(attribute : String) : Bool¶
Check if a specific attribute has changed
#changed?¶
Check if any attributes have changed
#changed_attributes¶
Get list of changed attributes
#changes¶
Get changes as a hash of attribute => [old, new]
#clear_changes_information¶
Mark all attributes as clean (no changes)
#errors¶
Errors object accessor (using private ivar name to avoid conflicts)
#new_record?¶
Check if this is a new record (not persisted)
#original_value(attribute : String) : DB::Any | Nil¶
Get original value of an attribute before changes
#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
#reload¶
Reload the record from the database
Example:
#reset_counter_cache!(counter_column : String, child_class, foreign_key : String)¶
Instance method to reset a counter cache
#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", "[email protected]")
#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).
#update¶
Update attributes and save the record
Example:
Macros¶
.__get_by_key_name(name)¶
Dynamic getter by string key name
.__set_by_key_name(name, value)¶
Dynamic setter by string key name Handles advanced types (JSON, UUID, Array, Enum) with proper type coercion
._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
._generate_preload_on_class¶
This is a macro that generates a proper typed method in subclasses
.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
.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
.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
.from_result_set(rs)¶
Create a model instance from a result set
.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))
.table(name)¶
Set the table name for this model
Nested Types¶
PrimaryKeyType-Default primary key type alias (Int64) - overridden by column macro when primary: true This allows associations to reference Model::PrimaryKeyType at compile time