How to cascade a DELETE in GORM?

3.2k Views Asked by At

I have tried everything I could think about to obtain a cascaded delete in GROM (deleting an object also deletes the tree of objects below). The code below is an attempt to do that and I tried with a HasMany, BelongsTo and HasMany+BelongsTo variant (by changing the types).

The current version is Hasmany+BelongsTo - the result is always the same: only the top object is soft-deleted (person) but nothing below

package main

import (
    "github.com/glebarez/sqlite"
    "github.com/rs/zerolog/log"
    "gorm.io/gorm"
)

type Person struct {
    gorm.Model
    Name  string
    Pills []Pill `gorm:"constraint:OnDelete:CASCADE"`
}

type Pill struct {
    gorm.Model
    Name       string
    PersonID   uint
    Person     Person
    Posologies []Posology `gorm:"constraint:OnDelete:CASCADE"`
}

type Posology struct {
    gorm.Model
    Name   string
    PillID uint
    Pill   Pill
}

func main() {
    var err error
    // setup database
    db, err := gorm.Open(sqlite.Open("test-gorm.db"), &gorm.Config{})
    if err != nil {
        log.Fatal().Msgf("failed to open follow-your-pills.db: %v", err)
    }
    // migrate database
    err = db.AutoMigrate(&Person{}, &Pill{}, &Posology{})
    if err != nil {
        log.Fatal().Msgf("failed to migrate database: %v", err)
    }
    log.Info().Msg("database initialized")

    db.Save(&Person{Name: "John"})
    db.Save(&Pill{Name: "Paracetamol", PersonID: 1})
    db.Save(&Pill{Name: "Aspirin", PersonID: 1})
    db.Save(&Posology{Name: "1x/day", PillID: 1})

    var person Person
    db.Find(&person)
    db.Delete(&person)
    log.Info().Msgf("person: %v", person)
}

Before giving up (something I really would like to avoid), or using a solution for a previous question (where OP ended up manually deleting all the objects) I would like to understand if I am completely missing the point of GORM (and its mechanisms) or if this is how it is and if I want to have cascaded deletes I will have to do them myself (which, as I think it now, may be the right solution after all because I see in the DB structure that DELETE CASCADE is part of the schema

CREATE TABLE `pills` (`id` integer,`created_at` datetime,`updated_at` datetime,`deleted_at` datetime,`name` text,`person_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_people_pills` FOREIGN KEY (`person_id`) REFERENCES `people`(`id`) ON DELETE CASCADE)

EDIT Following up on the last idea, I tried a

delete from people where id=1

and it correctly deleted the whole tree

2

There are 2 best solutions below

0
TiGo On

GORM does not automatically cascade deletes through associations by default. The OnDelete:CASCADE tag only adds a foreign key constraint in the database, but does not actually perform cascaded deletion at the application level.

To implement true cascaded deletes in GORM, you have to manually delete the associated records before deleting the parent record. For example:

// Delete all posologies for a pill
db.Where("pill_id = ?", pillID).Delete(&Posology{})

// Delete the pill 
db.Delete(&Pill{}, pillID) 

// Delete all pills for a person
db.Where("person_id = ?", personID).Delete(&Pill{})

// Finally delete the person
db.Delete(&Person{}, personID)

You are right that for true cascaded deletes, you have to handle deleting child records yourself in your application code.

The constraints just allow you to enforce referential integrity at the database level, but don't cascade deletes across Go objects automatically.

So in summary:

OnDelete:CASCADE -> Database constraint for referential integrity Cascaded deletes in app logic -> Must delete children explicitly before parent Doing it manually gives you more control but requires extra code. So tradeoff is convenience vs flexibility.

0
Shahriar Ahmed On

gorm:"constraint:OnDelete:CASCADE" is used for hard delete. But gorm doesn't do hard delete by default if you don't enforce by clause.Unscoped(). It does soft delete by updating the deleted_at field. You can achieve soft delete cascade by after delete hook. Related github issue.

type ModelA struct {
    ID         uint
    ModelBList []ModelB
}

type ModelB struct {
    ID         uint
    ModelAID   uint
    ModelCList []ModelC
}

type ModelC struct {
    ID       uint
    ModelBID uint
}

func (m *ModelA) AfterDelete(tx *gorm.DB) (err error) {
    tx.Clauses(clause.Returning{}).Where("model_a_id = ?", m.ID).Delete(&ModelB{})
    return
}

func (m *ModelB) AfterDelete(tx *gorm.DB) (err error) {
    tx.Clauses(clause.Returning{}).Where("model_b_id = ?", m.ID).Delete(&ModelC{})
    return
}