Associations¶
Associations allow you to define relationships between your models, making it easy to navigate and interact with related data. Ralph supports the most common relationship types found in modern ORMs, similar to Active Record.
Relationship Types¶
Ralph supports three main types of associations:
- One-to-One: A record in one table is associated with exactly one record in another table.
- One-to-Many: A record in one table is associated with zero or more records in another table.
- Many-to-Many: Records in one table are associated with multiple records in another table, usually through a join table.
belongs_to¶
A belongs_to association sets up a many-to-one relationship from the current model to another model. This model contains the foreign key.
class Post < Ralph::Model
table :posts
column id : Int64, primary: true
column title : String
column user_id : Int64
belongs_to user : User
end
Generated Methods¶
When you define belongs_to user : User, Ralph generates several methods for you:
user: Returns the associated user (ornil).user=(record): Sets the associated user and updates the foreign key.build_user(**attrs): Returns a new, unsavedUserobject.create_user(**attrs): Creates and saves a newUserobject.user_id_changed?: Returns true if the foreign key has been modified.user_id_was: Returns the original foreign key value before changes.
Options¶
foreign_key: The name of the foreign key column (defaults to#{association}_id).primary_key: The primary key on the associated model (defaults toid).optional: Iftrue, the foreign key can be nil (defaults tofalse).touch: Iftrue, updates the parent'supdated_atwhen this record is saved. You can also provide a specific column name.counter_cache: Iftrue, maintains a count of these records on the parent model (requires a#{table_name}_countcolumn on the parent).
belongs_to author : User, foreign_key: "author_id", touch: true
belongs_to category : Category, optional: true
has_one¶
A has_one association sets up a one-to-one relationship where the other model contains the foreign key.
class User < Ralph::Model
table :users
column id : Int64, primary: true
column name : String
has_one profile : Profile
end
Generated Methods¶
profile: Returns the associated profile (ornil).profile=(record): Sets the associated profile, updates its foreign key, and saves it.build_profile(**attrs): Returns a new, unsavedProfileobject with the foreign key set.create_profile(**attrs): Creates, saves, and returns a newProfileobject.
Options¶
foreign_key,primary_key: Same asbelongs_to.dependent: Controls what happens to the associated record when this record is destroyed. Options::destroy,:delete,:nullify,:restrict_with_error,:restrict_with_exception.
has_many¶
A has_many association sets up a one-to-many relationship.
Generated Methods¶
posts: Returns anArrayof associated posts.posts_any?: Returns true if there are any associated posts.posts_empty?: Returns true if there are no associated posts.build_post(**attrs): Builds a new post for this user.create_post(**attrs): Creates and saves a new post for this user.
Scoping Associations¶
You can provide a block to has_many to apply a scope to the association:
Options¶
through: Sets up a many-to-many relationship through another association.source: The name of the association on the "through" model to use as the source.dependent: Options::destroy,:delete_all,:nullify,:restrict_with_error,:restrict_with_exception.
Polymorphic Associations¶
Polymorphic associations allow a model to belong to more than one other model on a single association.
Setting up the "Belongs" side¶
Use polymorphic: true in your belongs_to definition. This requires both an _id and _type column.
class Comment < Ralph::Model
column id : Int64, primary: true
column body : String
column commentable_id : Int64
column commentable_type : String
belongs_to commentable : Model, polymorphic: true
end
Setting up the "Has" side¶
Use the as option to point to the polymorphic interface.
class Post < Ralph::Model
has_many comments : Comment, as: :commentable
end
class Video < Ralph::Model
has_many comments : Comment, as: :commentable
end
Registering Polymorphic Types¶
Because Crystal is a compiled language, you must explicitly register models that can be used in polymorphic associations if they are not automatically detected:
Ralph::Associations.register_polymorphic_type("Post", ->(id : Int64) { Post.find(id).as(Ralph::Model?) })
(Note: Ralph usually handles this automatically for you when using the macros.)
Through Associations¶
Through associations are used to define many-to-many relationships or to reach through an intermediate model.
class Physician < Ralph::Model
has_many appointments : Appointment
has_many patients : Patient, through: :appointments
end
class Appointment < Ralph::Model
belongs_to physician : Physician
belongs_to patient : Patient
end
class Patient < Ralph::Model
has_many appointments : Appointment
has_many physicians : Physician, through: :appointments
end
Association Loading¶
Ralph prioritizes explicit over implicit behavior. It never performs lazy loading of associations in a way that would cause unexpected N+1 queries.
Explicit Loading¶
When you access an association method like user.posts, Ralph will execute a query to fetch the related records if they haven't been preloaded.
Eager Loading¶
To avoid N+1 queries, use the preload method to eager load associations. This will fetch all related records in a separate query using an IN clause.
users = User.all
User.preload(users, :posts)
users.each do |user|
# These calls do not trigger additional database queries
puts "#{user.name} has #{user.posts.size} posts"
end
N+1 Query Tracking¶
Ralph can track and warn you about N+1 queries during development. This is disabled by default.