Skip to content
Relationships

Relationships

Relationships let you model the associations between tables (a user has many posts, a post belongs to a user) as fields on a model. They are declared with the builder package’s generic relationship types and are managed by the builder: the model package skips fields whose type implements relationship.Relationship during column collection, so no db tag is needed on the field and it never becomes a column.

Relationship types

There are three kinds of relationship:

type Author struct {
	model.BaseModel

	ID    int                     `db:"id,primary,autoincrement"`
	Name  string                  `db:"name"`
	Books *builder.HasMany[*Book] `json:"books"`
}

type Book struct {
	model.BaseModel

	ID       int                       `db:"id,primary,autoincrement"`
	AuthorID int                       `db:"author_id"`
	Author   *builder.BelongsTo[*Author] `json:"author"`
}

Author.Books is a has-many relationship (the books table stores the foreign key) and Book.Author is a belongs-to relationship (the books table stores the key that points back at authors). Both describe the same association from opposite ends.

Naming and keys

By default the columns are inferred from the model’s table name and primary key, so no tags are required when your schema follows the convention:

  • HasOne / HasMany: the parent is joined on its primary key (id), and the related table holds a foreign key named <singular parent table>_<parent key>, e.g. author_id.
  • BelongsTo: the parent table holds a foreign key named <singular related table>_<related key>, e.g. author_id, which references the primary key (id) of the related table.

Use struct tags to point at non-default columns:

tagrelationshipsdescription
localHasOne, HasManycolumn on the parent model the relationship joins on (default: parent primary key)
foreignHasOne, HasManycolumn on the related model holding the key (default: <singular parent table>_<parent key>)
foreignBelongsTocolumn on the parent model holding the key (default: <singular related table>_<related key>)
ownerBelongsTocolumn on the related model that the key references (default: related primary key)
type Book struct {
	model.BaseModel

	ID        int              `db:"id,primary,autoincrement"`
	PublisherID int            `db:"publisher_id"`
	Publisher *builder.BelongsTo[*Publisher] `db:"-" foreign:"publisher_id" owner:"id"`
}

If either model uses a composite primary key you must specify the keys with tags, because the defaults can only generate a single column.

Accessing loaded values

Every relationship field exposes two methods:

  • Value returns (T, bool) — the related value and whether it has been fetched.
  • Loaded reports whether the relationship has been fetched.
authors, err := builder.From[Author]().With("Books").Get(db)
// ...
for _, author := range authors {
	books, ok := author.Books.Value()
	if ok {
		// process books
	}
}

When a model is serialized to JSON, a relationship that has not been loaded encodes as null, while a loaded one encodes as its value.

Loading relationships

Eager loading with With

With registers relationships to be loaded in the queries that Get runs. Nested relationships are separated by dots:

authors, err := builder.From[Author]().
	With("Books", "Books.Chapters").
	Get(db)

Loading after the fact

Load loads a relationship onto already-fetched models, filling it in on every model in a slice. LoadMissing does the same while skipping relationships that have already been loaded:

err := builder.Load(db, authors, "Books")
err = builder.LoadMissing(db, authors, "Books")

Both support dotted paths for nested relationships.

Constraining queries with WhereHas

WhereHas adds an EXISTS condition so a query only returns rows whose related records exist and match the callback. The callback constrains the relationship as a subquery:

authors, err := builder.From[Author]().
	WhereHas("Books", func(q *builder.Builder) *builder.Builder {
		return q.Where("published", "=", true)
	}).
	Get(db)

Migrations

When spice generate:migration creates a migration for a model, each BelongsTo field contributes a foreign-key constraint referencing the related model’s table and key. Has-one and has-many relationships do not create constraints: the foreign key lives on the other table, and is declared on that model instead.