define-relation
Use this skill when defining ROM Relations that map to database tables in Hanami 2.x.
Core principle: The Relation is the query layer. It defines how to read and filter data, not business logic.
Quick Reference
| Scenario | Approach |
|---|---|
| Define a Relation | Class inherits from Hanami::DB::Relation with schema :table_name, infer: true |
| Infer schema from DB | schema :table_name, infer: true |
| Define explicit schema | schema :table_name do { attribute :id, Types::Integer } end |
| Add a custom query method | Define a method that returns a restricted/ordered relation |
| Define an association | many_to_one :users, as: :user or one_to_many :posts, as: :posts |
| Access the Relation in an Action | include Deps["relations.users"] |
| Call a query method | relations.users.active or relations.users.by_email("a@b.com") |
| Verify inferred schema | bundle exec hanami console → app['relations.users'].schema |
Core Rules
Create the Relation file in the app or slice:
# app/relations/users.rb # frozen_string_literal: true module MyApp module Relations class Users < Hanami::DB::Relation schema :users, infer: true end end endUse
infer: truefor new tables where the database schema is the source of truth. ROM will introspect the table at boot.Define explicit schema when you need custom type coercion or virtual attributes:
schema :users do attribute :id, Types::Integer attribute :email, Types::String attribute :created_at, Types::Time endAdd query methods for reusable filters. Encapsulate all query filters inside public relation methods — keep business logic, validations, and side effects out of Relations:
def active where(status: "active") end def by_email(email) where(email: email) endDefine associations: Use the association DSL macros for basic and advanced linkage between tables:
associations do # Basic cases many_to_one :users, as: :author one_to_many :posts, as: :posts # Aliased foreign key many_to_one :users, foreign_key: :author_id, as: :author # Nested / through association one_to_many :comments, through: :posts endLoading strategies: use
combineto eagerly load associations in a single composed query, orloadto issue a separate query:# In a repository or relation method: users.combine(:posts) # eager-load posts into each user struct users.combine(posts: :comments) # nested eager-loadKeep Relations in sync with migrations: If not using
infer: true, manually sync explicit schema declarations when migrations alter the database. Verify using the console:bundle exec hanami console # check app["relations.users"].schemaIf schema mismatch or boot failure: check migration status → run pending migrations (
bundle exec hanami db migrate) → restart the console → re-verify the schema. Ifinfer: truefails at boot, confirm the table exists in the database and that the connection configuration is correct.
Common Mistakes
| Mistake | Resolution |
|---|---|
| Redundant declarations | Do not declare explicit attributes for columns that are already auto-inferred (causes boot errors). |
| Direct View access | Bypassing the Repository layer to query relations directly in views is an anti-pattern. |
| Singular class names | Relations map to plural database tables and must be plural: Users, not User. |
For detailed relation pattern examples, see RELATIONS.md.
Integration
| Related Skill | When to chain |
|---|---|
| write-migration | After any migration that changes the schema — verify Relation still matches |
| create-repository | When you need domain-level read/write operations that wrap Relations |
| define-entity | When defining the data structures returned by Relations |
| write-rom-spec (testing) | When writing tests for Relation query methods |
| build-crud-resource | Full end-to-end: Relation → Repository → Action → View → Test |