Skip to content

Association

Defining Association

Association in REL can be declared by ensuring that each association have an association field, reference id field and foreign id field. Association field is a field with the type of another struct. Reference id is an id field that can be mapped to the foreign id field in another struct. By following that convention, REL currently supports belongs to, has one and has many association.

// User schema.
type User struct {
    ID        int
    Name      string
    Age       int
    CreatedAt time.Time
    UpdatedAt time.Time

    // has many transactions.
    // with custom reference and foreign field declaration.
    // ref: id refers to User.ID field.
    // fk: buyer_id refers to Transaction.BuyerID
    Transactions []Transaction `ref:"id" fk:"buyer_id"`

    // has one address.
    // doesn't contains primary key of other struct.
    // REL can guess the reference and foreign field if it's not specified.
    // autosave tag tells rel to automatically save the association when the parent is inserted/updated/deleted.
    // autoload tag tells rel to automatically load the association when record is queried.
    // alternatively you can use auto to enable both autoload and autosave.
    Address Address `autosave:"true" autoload:"true"`
}

// Transaction schema.
type Transaction struct {
    ID     int
    Item   string
    Status string

    // belongs to user.
    // contains primary key of other struct.
    Buyer   User `ref:"buyer_id" fk:"id"`
    BuyerID int
}

// Address schema.
type Address struct {
    ID   int
    City string

    // belongs to user.
    User   *User
    UserID *int
}

Preloading Association

Preload will load association to structs. To preload association, use Preload.

Preload Transaction's Buyer (belongs to association):

err := repo.Preload(ctx, &transaction, "buyer")
user := User{ID: 1, Name: "Nabe"}
repo.ExpectPreload("buyer").Result(user)

Preload User's Address (has one association):

err := repo.Preload(ctx, &user, "address")
address := Address{ID: 1, City: "Nazarick"}
repo.ExpectPreload("address").Result(address)

Preload User's Transactions (has many association):

err := repo.Preload(ctx, &user, "transactions")
transactions := []Transaction{
    {ID: 1, Item: "Avarice and Generosity", Status: "paid"},
}
repo.ExpectPreload("transactions").Result(transactions)

Preload only paid Transactions from users:

err := repo.Preload(ctx, &user, "transactions", where.Eq("status", "paid"))
transactions := []Transaction{
    {ID: 1, Item: "Avarice and Generosity", Status: "paid"},
}
repo.ExpectPreload("transactions", where.Eq("status", "paid")).Result(transactions)

Preload every Buyer's Address in Transactions (Buyer needs to be preloaded before preloading Buyer's Address):

err := repo.Preload(ctx, &transaction, "buyer.address")
userID := 1
addresses := []Address{{ID: 1, City: "Nazarick", UserID: &userID}}
repo.ExpectPreload("buyer.address").Result(addresses)

Preload also support slice, preload multiple transactions at once:

err := repo.Preload(ctx, &transaction, "buyer.address")
userID := 1
addresses := []Address{{ID: 1, City: "Nazarick", UserID: &userID}}
repo.ExpectPreload("buyer.address").Result(addresses)

Joining Association

Beside manually joining table using Join query, REL can also detect which field to join by using JoinAssoc method. It's also possible to load the result of joined table for has many and belongs to association by specifying it in the Select query.

Query Transaction with Buyer (belongs to association):

// Select "buyer.*" field tell REL to load the joined table to result as well.
err := repo.FindAll(ctx, &transactions, rel.Select("*", "buyer.*").JoinAssoc("buyer"))
transactions := []Transaction{{ID: 1, BuyerID: 2, Buyer: User{ID: 2, Name: "Nabe"}}}
repo.ExpectFindAll(rel.Select("*", "buyer.*").JoinAssoc("buyer")).Result(transactions)

Inserting and Updating Association

REL can automatically modifies association when it's parent is modified. If ID of association struct is not a zero value, REL will try to update the association, else it'll create a new association.

Note

Autosave feature needs to be explicitly enabled by adding (autosave:"true") tag to the struct definition.

Example: see User.Address struct.

user := User{
    Name: "rel",
    Address: Address{
        City: "Bandung",
    },
}

// Inserts a new record to users and address table.
// Result: User{ID: 1, Name: "rel", Address: Address{ID: 1, City: "Bandung", UserID: 1}}
err := repo.Insert(ctx, &user)
repo.ExpectInsert().ForType("main.User")

REL will try to update a new record for association if ID is a zero value. To update association, it first needs to be preloaded.

userID := 1
user := User{
    ID:   1,
    Name: "rel",
    // association is loaded when the primary key (id) is not zero.
    Address: Address{
        ID:     1,
        UserID: &userID,
        City:   "Bandung",
    },
}

// Update user record with id 1.
// Update address record with id 1.
err := repo.Update(ctx, &user)
repo.ExpectUpdate().ForType("main.User")

To selectively update only specific fields or association, use rel.Map.

mutation := rel.Map{
    "address": rel.Map{
        "city": "bandung",
    },
}

// Update address record if it's loaded else it'll creates a new address.
// only set city to bandung.
err := repo.Update(ctx, &user, mutation)
mutation := rel.Map{
    "address": rel.Map{
        "city": "bandung",
    },
}

// Update address record with id 1, only set city to bandung.
repo.ExpectUpdate(mutation).ForType("main.User")

Auto Saving and Loading Association

REL supports automatic loading or saving of association in structs by specifying the following struct tags:

  • autosave="true": Enables automatic saving of the association whenever parent is inserted/updated/deleted.
  • autoload="true": Enables automatic preloading of the association whenever parent is queried.
  • auto="true": Shorthand for enabling both autosave="true" and autoload="true".

Last update: 2024-08-16