Describe the feature
I would like to request support for belongs to polymorphic association in GORM. Currently, GORM provides support for many common association types, such as many-to-many, but lacks built-in support for belongs to polymorphic association.
Belongs to polymorphic association is a commonly used pattern where a model can belong to multiple other models, with the association determined by both the ID and the type of the associated model. This pattern allows for more flexibility and reusability in the database schema.
Motivation
type User struct {
gorm.Model
Name string
RoleID uint
RoleType string
Role interface{} `gorm:"-"`
}
type Role struct {
gorm.Model
Name string
}
type Admin struct {
Role
Level string
}
type Member struct {
Role
Rank string
}
i want to be able to preload Member
or Admin
into Role on User Table.
Using Raw Queries ?
I manually tried using raw queries to join the tables but it gets tricky and complex when you have a more complex Model with nested Polymorphic Model and Gorm doesn't allow you to Scan data Into an Interface
What am i missing ?
Related Issues
https://github.com/go-gorm/gorm/issues/3877
Comment From: TheDhejavu
Another slightly related issue
Example
type Content struct {
gorm.Model
Criteria []Criteria `gorm:"foreignKey:ContentID;"`
}
type Criteria struct {
gorm.Model
ContentID int
CriteriaInfoType string
CriteriaInfoID uint
CriteriaInfo interface{} `gorm:"-"`
}
type CriteriaA struct {
gorm.Model
Name string
Criteria *Criteria `gorm:"polymorphic:CriteriaInfo;polymorphicValue:CriteriaA"`
}
type CriteriaB struct {
gorm.Model
Name string
Criteria *Criteria `gorm:"polymorphic:CriteriaInfo;polymorphicValue:CriteriaB"`
}
The Content
struct has a slice of Criteria
, and I can preload the Criteria
data using GORM's Preload
function. However, the challenge lies in parsing CriteriaA
and CriteriaB
into the CriteriaInfo
field. Currently, I need to manually loop through the data and retrieve the necessary information causing N+1 problem. I'm wondering if there's a way to utilize a single query with GORM to preload the CriteriaInfo
as well.
### I don't want to do this
var content Content
db.Preload("Criteria.Criteria").First(&content)
for i := range content.Criteria {
criteria := content.Criteria[i]
switch criteria.CriteriaInfoType {
case "CriteriaA":
var criteriaA CriteriaA
db.Model(&criteriaA).Where("id = ?", criteria.CriteriaInfoID).First(&criteriaA)
criteria.CriteriaInfo = criteriaA
case "CriteriaB":
var criteriaB CriteriaB
db.Model(&criteriaB).Where("id = ?", criteria.CriteriaInfoID).First(&criteriaB)
criteria.CriteriaInfo = criteriaB
}
}
This will become slow if we need to perform more complex queries.
maybe this ?
var content Content
db.Preload("Criteria").Preload("Criteria.CriteriaInfo").First(&content)
i love gorm but it's becoming extremely hard to use when dealing with complex queries, What am i missing ?
Comment From: TheDhejavu
Cc @jinzhu
Comment From: TheDhejavu
Took a look at this once again.
Idea ?
type Content struct {
gorm.Model
Criteria []Criteria `gorm:"foreignKey:ContentID;"`
}
type Criteria struct {
gorm.Model
ContentID int
CriteriaInfoType string
CriteriaInfoID uint
CriteriaA CriteriaA `gorm:"joins:criteria_info_id:id, polymorpic"`
CriteriaB CriteriaB `gorm:"joins:criteria_info_id:id, polymorpic"`
}
type CriteriaA struct {
gorm.Model
Name string
Criteria *Criteria `gorm:"polymorphic:CriteriaInfo;polymorphicValue:CriteriaA"`
}
type CriteriaB struct {
gorm.Model
Name string
Criteria *Criteria `gorm:"polymorphic:CriteriaInfo;polymorphicValue:CriteriaB"`
}
Query ?
var content Content
db.Preload("Criteria").Preload("Criteria.CriteriaA").Preload("Criteria.CriteriaB").First(&content)
@jinzhu @a631807682
Is there something i am missing in the doc. Is this currently achievable ?
Comment From: a631807682
This seems to be implemented (nested preload), needs to investigate. cc @black-06
Comment From: a631807682
@TheDhejavu I think this works fine, is that what you want?
type Content struct {
gorm.Model
Criteria []Criteria `gorm:"foreignKey:ContentID;"`
}
type Criteria struct {
gorm.Model
ContentID int
CriteriaInfoType string
CriteriaInfoID uint
CriteriaA CriteriaA `gorm:"foreignKey:ID"`
CriteriaB CriteriaB `gorm:"foreignKey:ID"`
}
type CriteriaA struct {
gorm.Model
Name string
Criteria *Criteria `gorm:"polymorphic:CriteriaInfo;polymorphicValue:CriteriaA"`
}
type CriteriaB struct {
gorm.Model
Name string
Criteria *Criteria `gorm:"polymorphic:CriteriaInfo;polymorphicValue:CriteriaB"`
}
func TestGORM(t *testing.T) {
DB.Migrator().DropTable(&Content{}, &Criteria{}, &CriteriaA{}, &CriteriaB{})
err := DB.Migrator().AutoMigrate(&Content{}, &Criteria{}, &CriteriaA{}, &CriteriaB{})
if err != nil {
t.Fatal(err)
}
DB.Create(&Content{
Criteria: []Criteria{{CriteriaA: CriteriaA{Name: "A"}, CriteriaB: CriteriaB{Name: "B"}}},
})
var content Content
DB.Preload("Criteria").Preload("Criteria.CriteriaA").Preload("Criteria.CriteriaB").First(&content)
}
Comment From: TheDhejavu
@a631807682 no. the goal is to join Criteria WHERE criteria.criteria_info_id
=. criteria_a.id
and Preload the data into CriteriaA
OR WHERE criteria.criteria_info_id
=. criteria_b.id
and Preload it into CriteriaB
Comment From: a631807682
This is what you want, right? If not, please provide a reproduction link, indicate what result you want, and what is the current result.
type Content struct {
gorm.Model
Criteria []Criteria `gorm:"foreignKey:ContentID;"`
}
type Criteria struct {
gorm.Model
ContentID int
CriteriaInfoType string
CriteriaInfoID uint
CriteriaA CriteriaA `gorm:"foreignKey:ID;references:CriteriaInfoID"`
CriteriaB CriteriaB `gorm:"foreignKey:ID;references:CriteriaInfoID"`
}
type CriteriaA struct {
gorm.Model
Name string
Criteria *Criteria `gorm:"polymorphic:CriteriaInfo;polymorphicValue:CriteriaA"`
}
type CriteriaB struct {
gorm.Model
Name string
Criteria *Criteria `gorm:"polymorphic:CriteriaInfo;polymorphicValue:CriteriaB"`
}
func TestGORM(t *testing.T) {
DB.Migrator().DropTable(&Content{}, &Criteria{}, &CriteriaA{}, &CriteriaB{})
err := DB.Migrator().AutoMigrate(&Content{}, &Criteria{}, &CriteriaA{}, &CriteriaB{})
if err != nil {
t.Fatal(err)
}
DB.Create(&CriteriaA{Name: "A_1"}) // CriteriaInfoID == 1
DB.Create(&CriteriaB{Name: "B_1"}) // CriteriaInfoID == 1
DB.Create(&CriteriaB{Name: "B_2"}) // CriteriaInfoID == 2
DB.Create(&Content{
Criteria: []Criteria{{CriteriaInfoID: 1}, {CriteriaInfoID: 2}},
})
var content Content
DB.Preload("Criteria").Preload("Criteria.CriteriaA").Preload("Criteria.CriteriaB").First(&content)
if content.Criteria[0].CriteriaA.Name != "A_1" {
t.Fatal("Item 1 CriteriaA not found")
}
if content.Criteria[0].CriteriaB.Name != "B_1" {
t.Fatal("Item 1 CriteriaB not found")
}
if content.Criteria[1].CriteriaB.Name != "B_2" {
t.Fatal("Item 2 CriteriaB not found")
}
}
Comment From: TheDhejavu
Finally. Thank you man @a631807682 , this works!
Comment From: TheDhejavu
Hi @a631807682 , Preload function seems to generate a separate query for nested data thereby making it hard to perform even more complex queries that includes a fuzzy search on some columns.
E.G:
SELECT
"contents".*,
"criteria"."id" AS "criteria__id",
"criteria"."content_id" AS "criteria__content_id",
"criteria"."criteria_info_type" AS "criteria__criteria_info_type",
"criteria"."criteria_info_id" AS "criteria__criteria_info_id",
"criteria"."criteria_a_id" AS "criteria__criteria_a_id",
"criteria"."criteria_b_id" AS "criteria__criteria_b_id",
"criteria__criteria_a"."id" AS "criteria__criteria_a__id",
"criteria__criteria_a"."name" AS "criteria__criteria_a__name",
"criteria__criteria_a"."criteria_id" AS "criteria__criteria_a__criteria_id",
"criteria__criteria_b"."id" AS "criteria__criteria_b__id",
"criteria__criteria_b"."name" AS "criteria__criteria_b__name",
"criteria__criteria_b"."criteria_id" AS "criteria__criteria_b__criteria_id"
FROM
"contents"
LEFT JOIN
"criteria" ON ("criteria"."content_id" = "contents"."id")
LEFT JOIN
"criteria_a" AS "criteria__criteria_a" ON ("criteria__criteria_a"."id" = "criteria"."criteria_info_id" AND "criteria"."criteria_info_type" = 'CriteriaA')
LEFT JOIN
"criteria_b" AS "criteria__criteria_b" ON ("criteria__criteria_b"."id" = "criteria"."criteria_info_id" AND "criteria"."criteria_info_type" = 'CriteriaB')
WHERE
"criteria__criteria_a"."name" ILIKE '%A_1%'
i was hoping to see something like this?
Comment From: a631807682
use nested joins, their usage is the same
Comment From: TheDhejavu
use nested joins, their usage is the same
Thanks. Using joins requires lot of manual approach when scanning the data into its respective struct and that's what I am trying to prevent.
The above example is basic , the real usecase is larger than this.
Any other approach in Gorm that executes a single query ?
Comment From: TheDhejavu
Progress: i found this https://github.com/go-gorm/gorm/blob/master/tests/joins_test.go#L329
1:1
relationship seems to work but doesn't work for 1:N
and the relationship between content
and criteria
here is 1:N
Comment From: a631807682
join does not support many-to-many relationships refer to https://github.com/go-gorm/gorm/issues/5280#issuecomment-1514225741, you can use them separately.
Comment From: TheDhejavu
Thanks @a631807682
DB.Preload("Criteria.CriteriaA").
Preload("Criteria.CriteriaB").
Joins("LEFT JOIN criteria ON criteria.content_id = contents.id").
Joins("LEFT JOIN criteria_a AS criteria__criteria_a ON criteria__criteria_a.id = criteria.criteria_info_id AND criteria.criteria_info_type = 'CriteriaA'").
Joins("LEFT JOIN criteria_b AS criteria__criteria_b ON criteria__criteria_b.id = criteria.criteria_info_id AND criteria.criteria_info_type = 'CriteriaB'").
Where("criteria__criteria_a.name ILIKE ?", "%A_1%").
Find(&results)
I was able to combine Preload (preloading the 1:N criterias) and join to handle fuzzysearch.
Comment From: Fuzzyma
So there is no way, to only have "CriteriaInfo" without a or b and let it dynamically be filled with either Model A or Model B? Isnt the whole point of polymorphism to allow exactly that?
I need to access the parent of the relationship from the child model. So I would like to go from Criteria->Criteria{a,b} based on the criteria type.