# Associations

> **Supported web path:** Amber CLI `2.0.6` includes 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.

```crystal
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

```crystal
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.

```crystal
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.

```crystal
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.

```crystal
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.

```crystal
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.

```crystal
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.

```crystal
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.

```crystal
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.

```crystal
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.

```crystal
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.

```crystal
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)

```crystal
# 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

```crystal
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

```sql
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

```crystal
# 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
```