- docs/database: models, migrations, queries and pagination, relations, casts and validation, attachments and transactions (lagoon.Transaction, lagoon.AfterCommit, nested savepoints, lagoon.OnDatabase) - docs/services: configuration, events, routing with auth groups, rate limiting, authentication, the OAuth server, mail and localization - runnable Examples for lagoon, attach, compass, surf, wire, bouncer, wristband, postcard, phrasebook and festival; lagoon TestDocs* regions run on the package's Postgres harness through DocsDB - 15 new required pages
3.8 KiB
title, description, section, order
| title | description | section | order |
|---|---|---|---|
| Relations | Declare relations as GORM associations, write pivot tables with business columns explicitly, and cascade soft deletes inside the parent delete. | database | 40 |
Relations
Eloquent relations ($belongsTo, $hasMany, $belongsToMany) become GORM associations: struct fields whose type is another model, configured with gorm tags. Load them with Preload and filter through them with Joins, as the GORM association documentation describes. lagoon adds two helpers for the cases GORM leaves open.
Many-to-many with a pivot model
A many2many field joins two models through a join table. When the join table has columns of its own, such as a sort order or a role, describe it as a model:
// PostCategory is the pivot model: the join table has a business column.
type PostCategory struct {
PostID uint `gorm:"column:post_id;primaryKey"`
CategoryID uint `gorm:"column:category_id;primaryKey"`
SortOrder int `gorm:"column:sort_order"`
}
and register it for the field with lagoon.RegisterJoinTable, which calls GORM's SetupJoinTable and returns an error instead of panicking on a nil handle. GORM's association mode cannot set the extra columns, so write the pivot rows yourself: delete the post's rows and insert the new ones in one transaction, in the same transaction as the parent save when there is one. To read the rows in pivot order, join the pivot table:
if err := lagoon.RegisterJoinTable(db, &Post{}, "Categories", &PostCategory{}); err != nil {
return nil, err
}
err := lagoon.Transaction(ctx, db, func(ctx context.Context, tx *gorm.DB) error {
if err := tx.Where("post_id = ?", post.ID).Delete(&PostCategory{}).Error; err != nil {
return err
}
rows := make([]PostCategory, len(categoryIDs))
for i, id := range categoryIDs {
rows[i] = PostCategory{PostID: post.ID, CategoryID: id, SortOrder: i}
}
return tx.Create(&rows).Error
})
if err != nil {
return nil, err
}
var categories []Category
err = db.WithContext(ctx).
Joins("JOIN acme_blog_post_categories pc ON pc.category_id = acme_blog_categories.id").
Where("pc.post_id = ?", post.ID).
Order("pc.sort_order").
Find(&categories).Error
return categories, err
After lagoon.RegisterJoinTable, Preload("Categories") also loads the categories through the pivot model, without an order.
Warning
Do not order a
Preloadof a many-to-many field by a pivot column. GORM preloads the related rows in a query that does not join the pivot table, so the query fails. Use a join, as above.
Soft deletes and cascades
A model with a gorm.DeletedAt field is soft-deleted: Delete sets deleted_at, and queries skip deleted rows unless you call Unscoped. This is the SummerCMS form of the SoftDelete trait.
WinterCMS cascades a delete to dependent records through $hasMany options. In SummerCMS, the parent model does it in its BeforeDelete hook with lagoon.WithSoftDeleteCascade. GORM already runs the hook inside the transaction of the parent's delete, so the cascade commits or rolls back with it, and an error from the cascade aborts the parent delete:
// BeforeDelete soft-deletes the post's comments in the transaction of the
// post's own delete; an error aborts that delete.
func (p *Post) BeforeDelete(tx *gorm.DB) error {
return lagoon.WithSoftDeleteCascade(tx, func(tx *gorm.DB) error {
return tx.Where("post_id = ?", p.ID).Delete(&Comment{}).Error
})
}
Deleting a post then soft-deletes its comments in the same transaction. Files attached to a record are deleted differently, after the transaction commits; see Attachments.