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