Add support for polymorphic relationships using the POLYMORPHIC setting.

This commit adds support for two settings:

FOREIGNTYPE - A field that is used to store the type of the owner.

POLYMORPHIC - A shortcut to set FOREIGNKEY and FOREIGNTYPE to the same
value suffixed by "Id" and "Type" respectively.

The type is stored as the table name, which I thought might be useful
for other queries.

The biggest gotcha of this commit is that I flipped the definition of
has_one and belongs_to. gorm is very flexible such that it didn't
really care if it was a has_one or belongs_to, and can pretty much
determine it at runtime. For the sake of the error, I had to define
one of them as belongs_to, and I chose the one with the fields as
the belongs_to, like ActiveRecord. The error could probably be
genericized to "gorm cannot determine type", but I think it's nicer
to tell people DONT DO PATTERN XYZ CAUSE IT WONT WORK. Functionally,
it doesn't matter.
This commit is contained in:
jnfeinstein 2014-11-25 21:23:43 -08:00
parent 1de020dc4d
commit 8b451f0084
6 changed files with 80 additions and 25 deletions

View File

@ -7,11 +7,12 @@ import (
) )
type Association struct { type Association struct {
Scope *Scope Scope *Scope
PrimaryKey interface{} PrimaryKey interface{}
Column string PrimaryType interface{}
Error error Column string
Field *Field Error error
Field *Field
} }
func (association *Association) err(err error) *Association { func (association *Association) err(err error) *Association {
@ -172,7 +173,11 @@ func (association *Association) Count() int {
scope.db.Model("").Table(newScope.QuotedTableName()).Where(whereSql, association.PrimaryKey).Count(&count) scope.db.Model("").Table(newScope.QuotedTableName()).Where(whereSql, association.PrimaryKey).Count(&count)
} else if relationship.Kind == "has_many" || relationship.Kind == "has_one" { } else if relationship.Kind == "has_many" || relationship.Kind == "has_one" {
whereSql := fmt.Sprintf("%v.%v = ?", newScope.QuotedTableName(), newScope.Quote(ToSnake(relationship.ForeignKey))) whereSql := fmt.Sprintf("%v.%v = ?", newScope.QuotedTableName(), newScope.Quote(ToSnake(relationship.ForeignKey)))
scope.db.Model("").Table(newScope.QuotedTableName()).Where(whereSql, association.PrimaryKey).Count(&count) countScope := scope.db.Model("").Table(newScope.QuotedTableName()).Where(whereSql, association.PrimaryKey)
if relationship.ForeignType != "" {
countScope = countScope.Where(fmt.Sprintf("%v.%v = ?", newScope.QuotedTableName(), newScope.Quote(ToSnake(relationship.ForeignType))), association.PrimaryType)
}
countScope.Count(&count)
} else if relationship.Kind == "belongs_to" { } else if relationship.Kind == "belongs_to" {
if v, err := scope.FieldValueByName(association.Column); err == nil { if v, err := scope.FieldValueByName(association.Column); err == nil {
whereSql := fmt.Sprintf("%v.%v = ?", newScope.QuotedTableName(), newScope.Quote(ToSnake(relationship.ForeignKey))) whereSql := fmt.Sprintf("%v.%v = ?", newScope.QuotedTableName(), newScope.Quote(ToSnake(relationship.ForeignKey)))

View File

@ -35,6 +35,10 @@ func SaveBeforeAssociations(scope *Scope) {
if relationship.ForeignKey != "" { if relationship.ForeignKey != "" {
scope.SetColumn(relationship.ForeignKey, newDB.NewScope(value.Interface()).PrimaryKeyValue()) scope.SetColumn(relationship.ForeignKey, newDB.NewScope(value.Interface()).PrimaryKeyValue())
} }
if relationship.ForeignType != "" {
scope.Err(fmt.Errorf("gorm does not support polymorphic belongs_to associations"))
return
}
} }
} }
} }
@ -57,10 +61,17 @@ func SaveAfterAssociations(scope *Scope) {
if relationship.JoinTable == "" && relationship.ForeignKey != "" { if relationship.JoinTable == "" && relationship.ForeignKey != "" {
newDB.NewScope(elem).SetColumn(relationship.ForeignKey, scope.PrimaryKeyValue()) newDB.NewScope(elem).SetColumn(relationship.ForeignKey, scope.PrimaryKeyValue())
} }
if relationship.ForeignType != "" {
newDB.NewScope(elem).SetColumn(relationship.ForeignType, scope.TableName())
}
scope.Err(newDB.Save(elem).Error) scope.Err(newDB.Save(elem).Error)
if relationship.JoinTable != "" { if relationship.JoinTable != "" {
if relationship.ForeignType != "" {
scope.Err(fmt.Errorf("gorm does not support polymorphic many-to-many associations"))
}
newScope := scope.New(elem) newScope := scope.New(elem)
joinTable := relationship.JoinTable joinTable := relationship.JoinTable
foreignKey := ToSnake(relationship.ForeignKey) foreignKey := ToSnake(relationship.ForeignKey)
@ -89,6 +100,9 @@ func SaveAfterAssociations(scope *Scope) {
if relationship.ForeignKey != "" { if relationship.ForeignKey != "" {
newDB.NewScope(value.Addr().Interface()).SetColumn(relationship.ForeignKey, scope.PrimaryKeyValue()) newDB.NewScope(value.Addr().Interface()).SetColumn(relationship.ForeignKey, scope.PrimaryKeyValue())
} }
if relationship.ForeignType != "" {
newDB.NewScope(value.Addr().Interface()).SetColumn(relationship.ForeignType, scope.TableName())
}
scope.Err(newDB.Save(value.Addr().Interface()).Error) scope.Err(newDB.Save(value.Addr().Interface()).Error)
} else { } else {
destValue := reflect.New(field.Field.Type()).Elem() destValue := reflect.New(field.Field.Type()).Elem()
@ -101,6 +115,9 @@ func SaveAfterAssociations(scope *Scope) {
if relationship.ForeignKey != "" { if relationship.ForeignKey != "" {
newDB.NewScope(elem).SetColumn(relationship.ForeignKey, scope.PrimaryKeyValue()) newDB.NewScope(elem).SetColumn(relationship.ForeignKey, scope.PrimaryKeyValue())
} }
if relationship.ForeignType != "" {
newDB.NewScope(value.Addr().Interface()).SetColumn(relationship.ForeignType, scope.TableName())
}
scope.Err(newDB.Save(elem).Error) scope.Err(newDB.Save(elem).Error)
scope.SetColumn(field.Name, destValue.Interface()) scope.SetColumn(field.Name, destValue.Interface())
} }

View File

@ -10,6 +10,7 @@ import (
type relationship struct { type relationship struct {
JoinTable string JoinTable string
ForeignKey string ForeignKey string
ForeignType string
AssociationForeignKey string AssociationForeignKey string
Kind string Kind string
} }

View File

@ -406,6 +406,7 @@ func (s *DB) Association(column string) *Association {
scope := s.clone().NewScope(s.Value) scope := s.clone().NewScope(s.Value)
primaryKey := scope.PrimaryKeyValue() primaryKey := scope.PrimaryKeyValue()
primaryType := scope.TableName()
if reflect.DeepEqual(reflect.ValueOf(primaryKey), reflect.Zero(reflect.ValueOf(primaryKey).Type())) { if reflect.DeepEqual(reflect.ValueOf(primaryKey), reflect.Zero(reflect.ValueOf(primaryKey).Type())) {
scope.Err(errors.New("primary key can't be nil")) scope.Err(errors.New("primary key can't be nil"))
} }
@ -420,7 +421,7 @@ func (s *DB) Association(column string) *Association {
scope.Err(fmt.Errorf("%v doesn't have column %v", scope.IndirectValue().Type(), column)) scope.Err(fmt.Errorf("%v doesn't have column %v", scope.IndirectValue().Type(), column))
} }
return &Association{Scope: scope, Column: column, Error: s.Error, PrimaryKey: primaryKey, Field: field} return &Association{Scope: scope, Column: column, Error: s.Error, PrimaryKey: primaryKey, PrimaryType: primaryType, Field: field}
} }
// Set set value by name // Set set value by name

View File

@ -334,8 +334,15 @@ func (scope *Scope) fieldFromStruct(fieldStruct reflect.StructField, withRelatio
scopeTyp := scope.IndirectValue().Type() scopeTyp := scope.IndirectValue().Type()
foreignKey := SnakeToUpperCamel(settings["FOREIGNKEY"]) foreignKey := SnakeToUpperCamel(settings["FOREIGNKEY"])
foreignType := SnakeToUpperCamel(settings["FOREIGNTYPE"])
associationForeignKey := SnakeToUpperCamel(settings["ASSOCIATIONFOREIGNKEY"]) associationForeignKey := SnakeToUpperCamel(settings["ASSOCIATIONFOREIGNKEY"])
many2many := settings["MANY2MANY"] many2many := settings["MANY2MANY"]
polymorphic := SnakeToUpperCamel(settings["POLYMORPHIC"])
if polymorphic != "" {
foreignKey = polymorphic + "Id"
foreignType = polymorphic + "Type"
}
switch indirectValue.Kind() { switch indirectValue.Kind() {
case reflect.Slice: case reflect.Slice:
@ -359,6 +366,7 @@ func (scope *Scope) fieldFromStruct(fieldStruct reflect.StructField, withRelatio
field.Relationship = &relationship{ field.Relationship = &relationship{
JoinTable: many2many, JoinTable: many2many,
ForeignKey: foreignKey, ForeignKey: foreignKey,
ForeignType: foreignType,
AssociationForeignKey: associationForeignKey, AssociationForeignKey: associationForeignKey,
Kind: "has_many", Kind: "has_many",
} }
@ -400,7 +408,7 @@ func (scope *Scope) fieldFromStruct(fieldStruct reflect.StructField, withRelatio
kind = "has_one" kind = "has_one"
} }
field.Relationship = &relationship{ForeignKey: foreignKey, Kind: kind} field.Relationship = &relationship{ForeignKey: foreignKey, ForeignType: foreignType, Kind: kind}
} }
default: default:
field.IsNormal = true field.IsNormal = true

View File

@ -489,29 +489,52 @@ func (scope *Scope) related(value interface{}, foreignKeys ...string) *Scope {
foreignKey = keys[1] foreignKey = keys[1]
} }
var relationship *relationship
var field *Field
var scopeHasField bool
if field, scopeHasField = scope.FieldByName(foreignKey); scopeHasField {
relationship = field.Relationship
}
if scopeType == "" || scopeType == fromScopeType { if scopeType == "" || scopeType == fromScopeType {
if field, ok := scope.FieldByName(foreignKey); ok { if scopeHasField {
relationship := field.Relationship
if relationship != nil && relationship.ForeignKey != "" { if relationship != nil && relationship.ForeignKey != "" {
foreignKey = relationship.ForeignKey foreignKey = relationship.ForeignKey
if relationship.Kind == "many_to_many" {
joinSql := fmt.Sprintf(
"INNER JOIN %v ON %v.%v = %v.%v",
scope.Quote(relationship.JoinTable),
scope.Quote(relationship.JoinTable),
scope.Quote(ToSnake(relationship.AssociationForeignKey)),
toScope.QuotedTableName(),
scope.Quote(toScope.PrimaryKey()))
whereSql := fmt.Sprintf("%v.%v = ?", scope.Quote(relationship.JoinTable), scope.Quote(ToSnake(relationship.ForeignKey)))
toScope.db.Joins(joinSql).Where(whereSql, scope.PrimaryKeyValue()).Find(value)
return scope
}
} }
// has one if relationship != nil && relationship.Kind == "many_to_many" {
if relationship.ForeignType != "" {
scope.Err(fmt.Errorf("gorm does not support polymorphic many-to-many associations"))
}
joinSql := fmt.Sprintf(
"INNER JOIN %v ON %v.%v = %v.%v",
scope.Quote(relationship.JoinTable),
scope.Quote(relationship.JoinTable),
scope.Quote(ToSnake(relationship.AssociationForeignKey)),
toScope.QuotedTableName(),
scope.Quote(toScope.PrimaryKey()))
whereSql := fmt.Sprintf("%v.%v = ?", scope.Quote(relationship.JoinTable), scope.Quote(ToSnake(relationship.ForeignKey)))
toScope.db.Joins(joinSql).Where(whereSql, scope.PrimaryKeyValue()).Find(value)
return scope
}
// has many or has one
if toScope.HasColumn(foreignKey) {
toScope.inlineCondition(fmt.Sprintf("%v = ?", scope.Quote(ToSnake(foreignKey))), scope.PrimaryKeyValue())
if relationship != nil && relationship.ForeignType != "" && toScope.HasColumn(relationship.ForeignType) {
toScope.inlineCondition(fmt.Sprintf("%v = ?", scope.Quote(ToSnake(relationship.ForeignType))), scope.TableName())
}
toScope.callCallbacks(scope.db.parent.callback.queries)
return scope
}
// belongs to
if foreignValue, err := scope.FieldValueByName(foreignKey); err == nil { if foreignValue, err := scope.FieldValueByName(foreignKey); err == nil {
sql := fmt.Sprintf("%v = ?", scope.Quote(toScope.PrimaryKey())) sql := fmt.Sprintf("%v = ?", scope.Quote(toScope.PrimaryKey()))
if relationship != nil && relationship.ForeignType != "" && scope.HasColumn(relationship.ForeignType) {
scope.Err(fmt.Errorf("gorm does not support polymorphic belongs_to associations"))
return scope
}
toScope.inlineCondition(sql, foreignValue).callCallbacks(scope.db.parent.callback.queries) toScope.inlineCondition(sql, foreignValue).callCallbacks(scope.db.parent.callback.queries)
return scope return scope
} }
@ -519,7 +542,7 @@ func (scope *Scope) related(value interface{}, foreignKeys ...string) *Scope {
} }
if scopeType == "" || scopeType == toScopeType { if scopeType == "" || scopeType == toScopeType {
// has many // has many or has one in foreign scope
if toScope.HasColumn(foreignKey) { if toScope.HasColumn(foreignKey) {
sql := fmt.Sprintf("%v = ?", scope.Quote(ToSnake(foreignKey))) sql := fmt.Sprintf("%v = ?", scope.Quote(ToSnake(foreignKey)))
return toScope.inlineCondition(sql, scope.PrimaryKeyValue()).callCallbacks(scope.db.parent.callback.queries) return toScope.inlineCondition(sql, scope.PrimaryKeyValue()).callCallbacks(scope.db.parent.callback.queries)