The relationships you designed in Session 02, expressed in Go. GORM will load related rows for you — and the default way of asking is also the classic way to turn one query into two hundred.
8a
Association fields carry no column of their own. They tell GORM how to fetch rows from another table.
type Patient struct { ID uint FullName string // has one — the FK lives on the OTHER table (medical_records.patient_id) MedicalRecord *MedicalRecord `gorm:"foreignKey:PatientID"` // has many — same direction, many rows Appointments []Appointment `gorm:"foreignKey:PatientID"` } type Appointment struct { ID uint PatientID uint // the actual column DoctorID uint // belongs to — the FK lives on THIS table Patient Patient `gorm:"foreignKey:PatientID"` Doctor Doctor `gorm:"foreignKey:DoctorID"` } type Doctor struct { ID uint FullName string // many2many — GORM manages the join table for you Specialities []Speciality `gorm:"many2many:doctor_specialities"` }
appointments.patient_id points back.patient_id. Same relationship, opposite end.certified_on), model it as an explicit entity instead.Find leaves Patient as a zero struct and Appointments as nil — you must ask.8b
The N+1 problem is not exotic — it is what happens when you write the obvious loop. Walk it with Next and watch the query log.
Preload makes it two queries, always. GORM collects the foreign keys from the first result and fetches all the related rows with a single WHERE id IN (...).Preload("Doctor.Specialities") adds one more query, not one per doctor.Preload("Appointments", func(db *gorm.DB) *gorm.DB { return db.Where("status = ?", "booked") }) — a scope, in the place you would expect one.Turn the SQL log on in development and read it. logger.Default.LogMode(logger.Info) prints every statement. N+1 is obvious the moment you look, and essentially undetectable if you do not.
8c
Both fetch related data. They produce different SQL, different row counts, and one of them cannot express a one-to-many at all.
// belongs-to, and you need to FILTER on it → Joins. One statement, no duplication. db.Joins("Doctor"). Where("\"Doctor\".licence_no = ?", lic). Find(&appointments) // has-many → Preload. A join here would repeat each patient once per appointment. db.Preload("Appointments").Find(&patients) // both, nested, still bounded: 3 statements regardless of row count db.Preload("Appointments.Doctor").Find(&patients)
Joins. A Where alongside Preload filters the parent query, not the preload — and a filter inside the preload narrows the children without removing childless parents.Joins a has-many into a slice of models. A patient with four appointments comes back as four patient rows; GORM has no way to collapse them and you get duplicates.Select the columns you will actually serialise.(a) With logger.Info on, write the naive loop that lists tomorrow's appointments with each doctor's name, and count the statements in the log. (b) Fix it with Preload and confirm the count drops to two. (c) Now write "appointments whose doctor holds a cardiology speciality" — decide between Preload and Joins, implement it, and explain in one sentence why the other one gives the wrong answer.
Recap
Find leaves them zero.Preload makes it a fixed two statements with WHERE id IN (...).Joins to filter on a related table, Preload for has-many — joining a has-many duplicates the parent.