Associations#
Associations wire models together with belongs_to, has_one and has_many. Each macro generates accessor methods and hooks the relationship into the save/destroy lifecycle.
class Parent < PgORM::Base
attribute id : Int64
attribute name : String
has_many :children, class_name: Child
end
class Child < PgORM::Base
attribute id : Int64
attribute age : Int32
attribute parent_id : Int64?
belongs_to :parent
end
Note
The foreign key lives on the child table, and the macros don't declare it for you: the child model must declare it as an attribute (parent_id above).
All three macros accept the same overrides: class_name, foreign_key, autosave and dependent. has_many also accepts serialize. The class is inferred by camelcasing the name (has_many first strips a trailing s, so :children would become Children, which is why the example passes class_name). belongs_to infers the foreign key from the association name (parent_id); has_one and has_many infer it from the declaring class (Parent → parent_id).
belongs_to#
The child owns the foreign key. belongs_to :parent generates, on Child:
parent/parent?— returns the associated record (?returns nil instead of raising);parent=(record)— assigns it and setsparent_id(ifrecordisn't saved yet, the key is set when the child is saved);build_parent(**attrs)— builds an unsaved associated record;create_parent(**attrs)/create_parent!(**attrs)— builds and saves it (both raisePgORM::Error::RecordInvalidwhen validation fails);reload_parent— force a reload from the database.
child = Child.new(age: 6)
child.build_parent(name: "Phil") # unsaved; saved along with the child
child.parent = existing_parent # or assign a saved record: sets child.parent_id
child.save
The foreign key defaults to <name>_id and the class to the camelcased name; override with foreign_key: / class_name:.
has_one#
Declares a one-to-one where the other table holds the foreign key. has_one :supplier on Account generates supplier, supplier=, build_supplier, create_supplier, create_supplier! and reload_supplier. When the account is persisted, assigning through supplier= sets the foreign key on the new record and saves it, applying the dependent behaviour to the record it replaces (nullifying its foreign key when dependent isn't set). create_supplier and create_supplier! both raise PgORM::Error::RecordInvalid when validation fails, and PgORM::Error::RecordNotSaved if the account has no id yet.
account.create_supplier!(name: "ACME")
account.supplier # => #<Supplier account_id: 1>
has_many#
Declares a one-to-many. has_many :children on Parent generates a single method, children, returning a lazy PgORM::Relation. The relation is a full query scope — chain any query method onto it — and it can build and create children pre-wired to the parent:
parent = Parent.create!(name: "Phil")
parent.children.create(age: 6) # INSERT with parent_id set, raises RecordInvalid if invalid
parent.children.build(age: 3) # unsaved, parent_id set
parent.children.where_gt(:age, 5).to_a # queries the database
parent.children.to_a.size # => 2, includes the unsaved built child
create and create! behave the same: like Model.create, they raise PgORM::Error::RecordInvalid when validation fails (before pg-orm 2.5.2, create returned the unsaved record instead).
Once loaded (or after build/create), the relation caches its records, so to_a returns that cache, including unsaved built records, until you call reload. Chaining a query method such as where_gt runs a fresh query.
Pass serialize: true to include the loaded children in the parent's to_json output.
Generated method summary#
| Macro | Reader | Builders | Reload |
|---|---|---|---|
belongs_to |
name / name? |
build_name, create_name, create_name! |
reload_name |
has_one |
name / name? |
build_name, create_name, create_name! |
reload_name |
has_many |
name → Relation |
name.build, name.create, name.create! |
— |
Dependency handling#
dependent: decides what happens to associated records when the owner is destroyed. It is honoured by destroy (which wraps the work in a transaction), not by the callback-free delete.
| Option | Macros | Effect |
|---|---|---|
nil (default) |
all | do nothing |
:nullify |
has_one, has_many |
set the foreign key to NULL in SQL |
:delete |
belongs_to, has_one |
delete the associated row in SQL, no callbacks |
:delete_all |
has_many |
delete the associated rows in SQL, no callbacks |
:destroy |
all | call #destroy on each associated record, running its callbacks |
Warning
Any other value is silently ignored. In particular, has_many only understands :delete_all, not :delete. On a belongs_to, :delete and :destroy remove the parent record.
class Parent < PgORM::Base
attribute id : Int64
has_many :children, class_name: Child, dependent: :destroy
end
class Child < PgORM::Base
attribute id : Int64
attribute parent_id : Int64?
belongs_to :parent
has_many :pets, dependent: :delete_all
end
Autosave#
autosave: controls whether associated records are saved together with the owner:
nil(default) — save only newly built associations when the owner is saved;true— always save the association, new or persisted;false— never save it automatically.
For has_many, only records already held in the relation's cache (loaded, or added with build) are saved.
Lazy loading and joins#
Associations lazy-load: the associated record or relation is not fetched until you access it. When you drive the query through a join, PgORM caches the joined rows so that accessing the matching has_many association uses the cache instead of a second query. belongs_to and has_one don't use this cache.
Parent.where(id: parent.id).join(:left, Child, :parent_id).to_a.first
See Querying for joins in full.