Associations
Preview ecosystem guide: Grant is not part of the Amber 2.0.0-beta.2 core web-app release gate. Its package version, API, and platform support may change independently. Do not add a personal fork as a default dependency.
Grant provides powerful association methods to define relationships between models.
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