Skip to main content

Many-to-many

A many-to-many relation goes through a join table. Declare it on either side, or both, and migrations create the table once.

Declaring​

type Student struct {
ID int64
Name string
Courses []Course
}

func (Student) Configure(b *ant.ModelBuilder[Student]) {
b.ManyToMany(&St.Courses).Through("enrollments")
}

type Course struct {
ID int64
Title string
}

var (
St = ant.Fields[Student]()
Cs = ant.Fields[Course]()
)

The join table's columns default to student_id and course_id here. PivotColumns names them, which a self-referencing relation needs.

type Person struct {
ID int64
Friends []Person
}

func (Person) Configure(b *ant.ModelBuilder[Person]) {
b.ManyToMany(&Pn.Friends).Through("friendships").PivotColumns("person_id", "friend_id")
}

var Pn = ant.Fields[Person]()

Reading​

Include, Has, JoinMany, and CountRelated all go through the join table for you.

students, err := db.Students.Include(&St.Courses).All(ctx)

artists, err := db.Students.Where(ant.Has(&St.Courses, ant.Eq(&Cs.Title, "Art"))).All(ctx)

These change the join table only, never the rows on either side. Repeating a Link is harmless.

err := ant.Link(ctx, db, ann, &St.Courses, math, art) // add
err = ant.Unlink(ctx, db, ann, &St.Courses, art) // remove
err = ant.SyncLinks(ctx, db, ann, &St.Courses, art, music) // make the list exact
err = ant.Toggle(ctx, db, ann, &St.Courses, math) // flip each one

Through a tracked slice​

A loaded relation can be edited like any slice, and SaveChanges updates the links. A new target is inserted and linked. Removing one removes the link, never the course.

session := ant.Session(db)

student, err := session.Students.Include(&St.Courses).Find(ctx, id)

student.Courses = student.Courses[1:]
student.Courses = append(student.Courses, Course{Title: "Drama"})

err = session.SaveChanges(ctx)

A join table with data​

When the join table holds data of its own, like a grade or a date, it's really a model. Make it one, with two has-many relations, and you get queries, timestamps, and everything else for free. ant has no separate pivot API, on purpose.

type Enrollment struct {
ant.Base
StudentID int64
CourseID int64
Grade *string
}