Skip to main content

Advanced relations

These are the less common shapes a relation can take, each one configured in a single line.

Through another table​

HasManyThrough reaches across a table in the middle: a country's articles, by way of its members. It loads like any other relation, but it's never written, since those rows belong to the models in the middle.

type Country struct {
ID int64
Name string
Articles []Article
Top *Article
}

func (Country) Configure(b *ant.ModelBuilder[Country]) {
b.HasManyThrough(&Cn.Articles, &Mb.CountryID, &Ar.MemberID)
b.HasOneThrough(&Cn.Top, &Mb.CountryID, &Ar.MemberID)
}

type Member struct {
ID int64
CountryID int64
}

type Article struct {
ID int64
MemberID int64
Title string
}

var (
Cn = ant.Fields[Country]()
Mb = ant.Fields[Member]()
Ar = ant.Fields[Article]()
)

countries, err := db.Countries.Include(&Cn.Articles).All(ctx)

Polymorphic relations​

One foreign key for several tables, plus a second column saying which table. Essay 1 and Video 1 share a key, and each still sees only its own comments. Saving through the relation sets the type column for you.

type Comment struct {
ID int64
OwnerID int64
OwnerType string
Text string
Essay *Essay
Video *Video
}

func (Comment) Configure(b *ant.ModelBuilder[Comment]) {
b.BelongsTo(&Cm.Essay).ForeignKey(&Cm.OwnerID).Morph(&Cm.OwnerType, "essay")
b.BelongsTo(&Cm.Video).ForeignKey(&Cm.OwnerID).Morph(&Cm.OwnerType, "video")
}

type Essay struct {
ID int64
Comments []Comment
}

func (Essay) Configure(b *ant.ModelBuilder[Essay]) {
b.HasMany(&Es.Comments).ForeignKey(&Cm.OwnerID).Morph(&Cm.OwnerType, "essay")
}

type Video struct {
ID int64
Comments []Comment
}

var (
Cm = ant.Fields[Comment]()
Es = ant.Fields[Essay]()
)

What's stored is the value you give .Morph, so renaming a Go type can't change your data. Migrations write no foreign key here, since one column can't point at several tables.

Scoped relations and latest of many​

A relation can carry a condition, or pick one row per parent.

type Novelist struct {
ID int64
Novels []Novel
Live []Novel
Newest *Novel
}

func (Novelist) Configure(b *ant.ModelBuilder[Novelist]) {
b.HasMany(&Nv.Live).ForeignKey(&Nl.NovelistID).Where(ant.Eq(&Nl.Published, true))
b.HasOne(&Nv.Newest).ForeignKey(&Nl.NovelistID).Latest(&Nl.Year)
}

type Novel struct {
ID int64
NovelistID int64
Year int
Published bool
Novelist *Novelist
}

var (
Nv = ant.Fields[Novelist]()
Nl = ant.Fields[Novel]()
)

Counts and Has follow the scope. .Oldest is the opposite of .Latest.

Filling the way back​

Inverse points each child at its parent, with no query.

func (Novelist) Configure(b *ant.ModelBuilder[Novelist]) {
b.HasMany(&Nv.Novels).ForeignKey(&Nl.NovelistID).Inverse(&Nl.Novelist)
}

After Include(&Nv.Novels), novels[0].Novelist is the same instance as its parent.

Touching the parent​

Touch moves a parent's UpdatedAt when a child is written.

type Answer struct {
ID int64
DiscussionID int64
Discussion *Discussion
}

func (Answer) Configure(b *ant.ModelBuilder[Answer]) {
b.BelongsTo(&An.Discussion).Touch()
}

type Discussion struct {
ID int64
UpdatedAt time.Time
}

var An = ant.Fields[Answer]()