Gorm's half-baked, magic-out-the-box support of foreign keys has been an annoyance for years and I'm finally trying to figure it out once and for all. I'm using Postgres 12, gorm 1.23.3 and go 1.18.
I have a base model similar to gorm.Model but with a little bit extra:
type BaseModel struct {
ID string `json:"id" gorm:"type:uuid;primarykey;default:uuid_generate_v4()"`
InstanceVersion int `json:"instanceVersion"`
CreatedAt time.Time `json:"createdAt" gorm:"type:timestamp"`
UpdatedAt time.Time `json:"updatedAt" gorm:"type:timestamp"`
DeletedAt *time.Time `json:"deletedAt,omitempty" gorm:"type:timestamp" sql:"index"`
CreatedBy string `json:"createdBy"`
UpdatedBy string `json:"updatedBy"`
DeletedBy string `json:"deletedBy,omitempty"`
MetaData json.RawMessage `json:"metadata" gorm:"type:jsonb;default:'{}'"`
}
Every model in my DB uses this BaseModel as follows:
type Profile struct {
BaseModel
Name string `json:"name"`
UserID string `json:"userId"`
}
It generates the tables as follows (UML generated by DBeaver and double checked to be true):
I'm trying to add a foreign key to the CreatedBy and UpdatedBy columns such that they must point to an existing Profile. So I add the following field to the BaseModel type:
CreatedByProfile *Profile `json:"-" gorm:"foreignKey:CreatedBy"`
I expected the foreign key to be created for every model that BaseModel is a part of and point back to the Profiles table. However, it only makes the FK on the Profiles table.
Minimal recreation of the problem:
package main
import (
"encoding/json"
"time"
"github.com/lib/pq"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
type BaseModel struct {
ID string `json:"id" gorm:"type:uuid;primarykey;default:uuid_generate_v4()"`
InstanceVersion int `json:"instanceVersion"`
CreatedAt time.Time `json:"createdAt" gorm:"type:timestamp"`
UpdatedAt time.Time `json:"updatedAt" gorm:"type:timestamp"`
DeletedAt *time.Time `json:"deletedAt,omitempty" gorm:"type:timestamp" sql:"index"`
CreatedBy string `json:"createdBy"`
UpdatedBy string `json:"updatedBy"`
DeletedBy *string `json:"deletedBy,omitempty"`
MetaData json.RawMessage `json:"metadata" gorm:"type:jsonb;default:'{}'"`
CreatedByProfile *Profile `json:"-" gorm:"foreignKey:CreatedBy"`
}
type ActivityType string
type Activity struct {
Base BaseModel `gorm:"embedded"`
Type ActivityType `json:"type"`
Message string `json:"message"`
Content string `json:"content"`
ImageUrl string `json:"imageUrl"`
DisplayProfileIds pq.StringArray `json:"displayProfileIds" gorm:"type:uuid[]"`
RecipientProfileIds pq.StringArray `json:"recipientProfileIds" gorm:"-"`
// Preload
ActivityProfiles []*ActivityProfile `json:"activityProfiles"` // has many
}
type ActivityProfile struct {
Base BaseModel `gorm:"embedded"`
ReadAt *time.Time `json:"readAt,omitempty" gorm:"type:timestamp" sql:"index"`
Route string `json:"route"`
ActivityID string `json:"activityId"`
ProfileID string `json:"profileId"`
// Preload
Activity *Activity `json:"activity"` // belongs to
Profile *Profile `json:"profile"` // belongs to
}
type Profile struct {
BaseModel
Name string `json:"name"`
UserID string `json:"userId"`
}
func main() {
db, err := gorm.Open(postgres.Open("host=localhost port=5432 user=corey dbname=corey password= sslmode=disable"))
if err != nil {
panic(err)
}
models := []interface{}{
&Activity{},
&ActivityProfile{},
&Profile{},
}
err = db.AutoMigrate(models...)
if err != nil {
panic(err)
}
}
I've also tried using gorm:"embedded" tags instead of nested structs but it didn't help. Nothing in this question helps: DB.Model(...).AddForeignKey(...) doesn't exist anymore, the db.Migrator().CreateConstraint(...) lines don't work (how does it know what column is the FK and which column it matches to of what other type? Do I have to run both lines? How could this ever possibly work?!?), and I don't want OnUpdate or OnDelete constraints, just foreign keys.
If I put the CreatedByProfile field on Activity, then I get a FK where Profile.CreatedBy is an FK to Activity.ID which is 100% backwards.
I can add this field to my Profile model:
Activities []*Activity `json:"-" gorm:"foreignKey:CreatedBy"`
and it creates the FK like I want, but I don't actually want that field present on the model. Plus, I'd have to add this unnecessary boilerplate for every model in my DB (and _ fields don't end up with the FK being made).
How can I make Gorm do simple things like create foreign keys without decorating my models with unused fields?
Related
What is the difference between Has One, Has Many and Belong To
I have 3 Models
User
Profile Where profile and user should have one to one relationship
Category Where category should be foreign key to user
type User struct {
gorm.Model
Email *string
Name string
...
}
type Profile struct {
gorm.Model
Phone string
Address string
...
}
type Category struct {
gorm.Model
Name string
}
For User Has One Profile
type User struct {
gorm.Model
Email *string
Name string
Profile Profile //this is the key different
}
type Profile struct {
gorm.Model
UserId int //this is important
Phone string
Address string
}
For Profile Belong To User
type User struct {
gorm.Model
Email *string
Name string
}
type Profile struct {
gorm.Model
UserId int //this is important
User User //this is the key different
Phone string
Address string
}
For User Has Many Category
type User struct {
gorm.Model
Email *string
Name string
CategoryList []Category
}
type Category struct {
gorm.Model
UserId int //this is important
Name string
}
Edit: UserId field will become your foreign key.
If you want gorm to automatically create table for you, you can use AutoMigrate in main.go
err := db.AutoMigrate(your_model_package.User{})
if err != nil {
return err
}
When I try to insert to a table with gorm which has one to many relationship, I get this error:
Error 1452: Cannot add or update a child row: a foreign key constraint fails (`Todo`.`todos`, CONSTRAINT `fk_users_todo_list` FOREIGN KEY (`fk_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE)....
These are my models:
type Base struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
}
type User struct {
Base
FirstName string `form:"first_name" gorm:"type:varchar(20)"`
LastName string `form:"last_name" gorm:"type:varchar(20)"`
UserName string `form:"user_name" gorm:"unique;type:varchar(20)"`
Email string `form:"email" gorm:"type:varchar(100)"`
Password string `form:"password" gorm:"type:varchar(30)"`
Gender types.UserGender `form:"gender" gorm:"type:smallint"`
TodoList []Todo `gorm:"foreignKey:FkID;constraint:onDelete:SET NULL,onUpdate:CASCADE" json:"omitempty"`
}
type Todo struct {
Base
Name string
Description string
Status types.TaskStatus
FkID uint
}
And this is the function that I wrote:
func (d *DB) AddTODO(todo *models.Todo, userName string) error {
user := &models.User{UserName: userName}
err := d.db.Model(&user).Where("user_name = ?", userName).Association("TodoList").
Append([]models.Todo{*todo})
if err != nil {
return err
}
return nil
}
I'm using MariaDB.
It want user from gorm result. Change your method maybe like this:
// arg user is from gorm result somewhere
func (d *DB) AddTODO(user *models.User, todo *models.Todo) error {
return d.db.Model(user).Association("TodoList").Append([]models.Todo{*todo})
}
TodoList lack references keyword. It should be like this.
type User struct {
Base
FirstName string `form:"first_name" gorm:"type:varchar(20)"`
LastName string `form:"last_name" gorm:"type:varchar(20)"`
UserName string `form:"user_name" gorm:"unique;type:varchar(20)"`
Email string `form:"email" gorm:"type:varchar(100)"`
Password string `form:"password" gorm:"type:varchar(30)"`
Gender types.UserGender `form:"gender" gorm:"type:smallint"`
TodoList []Todo `gorm:"foreignKey:FkID;references:ID;constraint:onDelete:SET NULL,onUpdate:CASCADE" json:"omitempty"`
}
I am trying to add a custom attribute to my Golang struct just like how I usually add custom attribute on a Laravel model using the $appends variable.
This is the code:
package models
import (
"time"
)
type Dummy struct {
ID string `json:"id" gorm:"primary_key"`
Description string `json:"description"`
Image string `json:"image"`
ImageUrl ImageUrlDummy
Number int `json:"number"`
UserId string `json:"user_id"`
User User `json:"user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func ImageUrlDummy() string {
return "test"
}
However, the ImageUrlDummy inside the struct does not work, it return error such as:
ImageUrlDummy (value of type func() string) is not a type
How do I achieve this same code from Laravel to Golang?
class Dummy extends Model
{
protected $appends = ['image_url'];
public function getImageUrlAttribute(){
return "this is the image url";
}
}
Please pardon me I am still learning Golang, thank you
You are not far off..
Change your struct to (remove ImageUrlDummy, fix json tag for Image):
type Dummy struct {
ID string `json:"id" gorm:"primary_key"`
Description string `json:"description"`
Image string `json:"image"`
Number int `json:"number"`
UserId string `json:"user_id"`
User User `json:"user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Then define a method with a receiver of type Dummy pointer
func (d *Dummy) ImageUrl() string {
return "test"
}
Example with a few more things: https://play.golang.com/p/olGSFhBgqkG
I am playing with gin-gonic+sqlx. Now I wonder what is the best way to dynamically bind database records to the struct?
Saying I have a struct and a query like this.
type Project struct {
ID string `db:"id"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
DeletedAt NullTime `db:"deleted_at"`
Name string
Status string
OpenDate NullTime `db:"open_date"`
Number uint
}
func (p Project) List() ([]Project, error) {
var pp []Project
err := db.Select(&pp, "SELECT * FROM Project")
return pp, err
}
Now I want to add field-level permissions to this struct. For example, an admin can view and edit all fields; User A can view CreatedAt and Name; User B can view and edit Name Status and view Number.
I was writing multiply structs for each use case, but obviously, I did choose the silliest way.
The other method I can think of is to implement Struct Tag. Am I on the right track or is there a better way to do this?
We can create multiple structs and embed among each other.
Example:
type ProjectSummary struct {
ID string `db:"id"`
Name string
}
type ProjectUser struct {
ProjectSummary
CreatedAt time.Time `db:"created_at"`
}
type ProjectSupport struct {
ProjectSummary
Status string
Number uint
}
type ProjectAdmin struct {
ProjectSupport
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
DeletedAt NullTime `db:"deleted_at"`
OpenDate NullTime `db:"open_date"`
}
I've two structs:
type UpdateableUser struct {
FirstName string
LastName string
Email string
Tlm float64
Dob time.Time
}
type User struct {
Id string
FirstName string
LastName string
Email string
DOB time.Time
Tlm float64
created time.Time
updated time.Time
}
Through a binder I bind request data to the updateableUser struct, therefore I might have an updateableUser with only one "real" value, like uu here:
uu := UpdateableUser{Lastname: "Smith"}
Now I want to set only the not "emtpy" values from UpdateableUser to User. Can you please give me a hint or more?
I would recommend embedding the Updateable struct into the larger struct:
type UpdateableUser struct {
FirstName string
LastName string
Email string
Tlm float64
Dob time.Time
}
type User struct {
UpdateableUser
ID string
created time.Time
updated time.Time
}
func (u *User) UpdateFrom(src *UpdateableUser) {
if src.FirstName != "" {
u.FirstName = src.FirstName
}
if src.LastName != "" {
u.LastName = src.LastName
}
// ... And other properties. Tedious, but simple and avoids Reflection
}
This allows you to use UpdateableUser as an interface to make it explicit which properties can be updated.