Associations
Supported web path: Amber CLI
2.0.6includes Grant in every generated web application and pins the reviewed V2 commit. Preserve that pin while following this beta.
Where the examples go
Association declarations and helper methods belong inside the matching Grant
model under src/models/, such as src/models/post.cr. Usage and eager-loading
expressions run from the controller, job, service, or spec that owns the
operation. SQL index examples belong in the migration system selected by the
application, not in a model file. Blocks on this page use those destinations
unless a closer comment identifies a different role.
Grant associations declare how models find related records and where the foreign key for that relationship lives.
belongs_to
Creates a one-to-one connection where the declaring model holds the foreign key.
class Post < Grant::Base
belongs_to :user
column id : Int64, primary: true
column title : String
column user_id : Int64 # Foreign key
end
# Usage
post = Post.find(1)
author = post.user # Fetches associated user
belongs_to Options
class Post < Grant::Base
# Custom foreign key
belongs_to user : User, foreign_key: author_id : Int64
# Optional association (allows NULL)
belongs_to :category, optional: true
# With counter cache
belongs_to :blog, counter_cache: true
# Touch parent on save
belongs_to :article, touch: true
# Custom class name
belongs_to :author, class_name: User
end
has_one
Creates a one-to-one connection where the other model holds the foreign key.
class User < Grant::Base
has_one :profile
column id : Int64, primary: true
column email : String
end
class Profile < Grant::Base
belongs_to :user
column id : Int64, primary: true
column bio : String
column user_id : Int64
end
# Usage
user = User.find(1)
profile = user.profile
user.profile = Profile.new(bio: "My bio")
has_many
Creates a one-to-many connection.
class User < Grant::Base
has_many :posts
has_many :comments
# With custom foreign key
has_many :articles, class_name: Post, foreign_key: :author_id
column id : Int64, primary: true
end
# Usage
user = User.find(1)
user.posts.each do |post|
puts post.title
end
# Add new post
user.posts << Post.new(title: "New Post")
has_many :through
Creates a many-to-many connection through a join model.
class User < Grant::Base
has_many :participations
has_many :rooms, through: :participations
column id : Int64, primary: true
column name : String
end
class Participation < Grant::Base
belongs_to :user
belongs_to :room
column id : Int64, primary: true
column joined_at : Time
column role : String # Additional attributes
end
class Room < Grant::Base
has_many :participations
has_many :users, through: :participations
column id : Int64, primary: true
column name : String
end
# Usage
user = User.find(1)
user.rooms.each { |room| puts room.name }
# Create association
Participation.create!(user: user, room: room, role: "member")
Polymorphic Associations
Allow a model to belong to multiple other models through a single association.
class Comment < Grant::Base
belongs_to :commentable, polymorphic: true
column id : Int64, primary: true
column content : String
column commentable_id : Int64?
column commentable_type : String?
end
class Post < Grant::Base
has_many :comments, as: :commentable
end
class Photo < Grant::Base
has_many :comments, as: :commentable
end
# Usage
post = Post.create!(title: "My Post")
photo = Photo.create!(url: "image.jpg")
comment1 = Comment.create!(content: "Great post!", commentable: post)
comment2 = Comment.create!(content: "Nice photo!", commentable: photo)
# Retrieve polymorphic association
comment = Comment.find(1)
if comment.commentable.is_a?(Post)
puts "Comment on post: #{comment.commentable.title}"
end
Self-Referential Associations
Models that have associations to themselves.
class Employee < Grant::Base
belongs_to :manager, class_name: Employee, optional: true
has_many :subordinates, class_name: Employee, foreign_key: :manager_id
column id : Int64, primary: true
column name : String
column manager_id : Int64?
end
# Usage
ceo = Employee.create!(name: "CEO")
manager = Employee.create!(name: "Manager", manager: ceo)
employee = Employee.create!(name: "Employee", manager: manager)
ceo.subordinates # => [manager]
manager.subordinates # => [employee]
employee.manager # => manager
Association Options
dependent
Controls what happens to associated records when parent is destroyed.
class Author < Grant::Base
# Destroys all posts when author is destroyed
has_many :posts, dependent: :destroy
# Sets category_id to NULL on products
has_many :products, dependent: :nullify
# Prevents deletion if players exist
has_many :players, dependent: :restrict
end
counter_cache
Maintains count of associated records on parent model.
class Blog < Grant::Base
column posts_count : Int32 = 0
has_many :posts
end
class Post < Grant::Base
belongs_to :blog, counter_cache: true
end
# Usage
blog = Blog.create!(title: "My Blog")
Post.create!(title: "First Post", blog: blog)
blog.reload.posts_count # => 1
touch
Updates parent's updated_at when child is saved.
class Comment < Grant::Base
belongs_to :post, touch: true
# Touch specific column
belongs_to :article, touch: :last_activity_at
end
# Updates post.updated_at whenever comment changes
comment.update!(content: "Updated")
autosave
Automatically saves associated records with parent.
class Order < Grant::Base
has_many :line_items, autosave: true
has_one :invoice, autosave: true
end
order = Order.new
order.line_items << LineItem.new(product: "Widget", qty: 2)
order.invoice = Invoice.new(total: 100)
order.save! # Saves everything in transaction
Nested Attributes
Accept nested attributes for associated records.
class Order < Grant::Base
has_many :line_items
accepts_nested_attributes_for line_items : LineItem,
allow_destroy: true,
reject_if: ->(attrs : Hash) { attrs["quantity"]?.try(&.to_i) == 0 },
limit: 50
end
# Create order with line items
order = Order.create!(
customer_id: 1,
line_items_attributes: [
{product_id: 1, quantity: 2},
{product_id: 3, quantity: 1}
]
)
Eager Loading (N+1 Prevention)
# Bad: N+1 queries
posts = Post.all
posts.each do |post|
puts post.author.name # Query for each post
end
# Good: Eager loading
posts = Post.includes(:author)
posts.each do |post|
puts post.author.name # No additional queries
end
# Multiple associations
posts = Post.includes(:author, :comments)
# Nested associations
users = User.includes(posts: [:comments, :tags])
Validating Associations
class Order < Grant::Base
has_many :line_items
belongs_to :customer
validates_associated :line_items
validate :must_have_items
private def must_have_items
if line_items.empty?
errors.add(:line_items, "must have at least one item")
end
end
end
Best Practices
1. Index Foreign Keys
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_blog_id ON posts(blog_id);
2. Use dependent Wisely
:destroy- When child records should be deleted:nullify- When child records can exist independently:restrict- When deletion should be prevented
3. Document Complex Associations
# Represents many-to-many between users and projects
# through team memberships with role attribute
class TeamMembership < Grant::Base
belongs_to :user
belongs_to :project
column role : String # "owner", "member", "viewer"
end