diff --git a/association.go b/association.go index defb2006..7ebe065e 100644 --- a/association.go +++ b/association.go @@ -2,7 +2,6 @@ package rel import ( "reflect" - "strings" "sync" "github.com/serenize/snaker" @@ -18,8 +17,6 @@ const ( HasOne // HasMany association. HasMany - // ManyToMany association. - ManyToMany ) type associationKey struct { @@ -28,16 +25,14 @@ type associationKey struct { } type associationData struct { - typ AssociationType - targetIndex []int - referenceField string - referenceIndex int - referenceThrough string - foreignField string - foreignIndex int - foreignThrough string - through string - autosave bool + typ AssociationType + targetIndex []int + referenceField string + referenceIndex int + foreignField string + foreignIndex int + through string + autosave bool } var associationCache sync.Map @@ -119,11 +114,6 @@ func (a Association) ReferenceField() string { return a.data.referenceField } -// ReferenceThrough return intermediary foreign field used for many to many association. -func (a Association) ReferenceThrough() string { - return a.data.referenceThrough -} - // ReferenceValue of the association. func (a Association) ReferenceValue() interface{} { return indirect(a.rv.Field(a.data.referenceIndex)) @@ -134,16 +124,11 @@ func (a Association) ForeignField() string { return a.data.foreignField } -// ForeignThrough return intermediary foreign field used for many to many association. -func (a Association) ForeignThrough() string { - return a.data.foreignThrough -} - // ForeignValue of the association. // It'll panic if association type is has many. func (a Association) ForeignValue() interface{} { - if a.Type() == HasMany || a.Type() == ManyToMany { - panic("cannot infer foreign value for has many or many to many association") + if a.Type() == HasMany { + panic("rel: cannot infer foreign value for has many or many to many association") } var ( @@ -157,7 +142,7 @@ func (a Association) ForeignValue() interface{} { return indirect(rv.Field(a.data.foreignIndex)) } -// Through return intermediary table used for many to many association. +// Through return intermediary association. func (a Association) Through() string { return a.data.through } @@ -191,18 +176,22 @@ func extractAssociationData(rt reflect.Type, index int) associationData { } var ( - sf = rt.Field(index) - ft = sf.Type - ref, refThrough = getAssocField(sf.Tag, "ref") - fk, fkThrough = getAssocField(sf.Tag, "fk") - through = sf.Tag.Get("through") - fName = fieldName(sf) - assocData = associationData{ + sf = rt.Field(index) + ft = sf.Type + ref = sf.Tag.Get("ref") + fk = sf.Tag.Get("fk") + fName = fieldName(sf) + assocData = associationData{ targetIndex: sf.Index, + through: sf.Tag.Get("through"), autosave: sf.Tag.Get("autosave") == "true", } ) + if assocData.autosave && assocData.through != "" { + panic("rel: autosave is not supported for has one/has many through association") + } + for ft.Kind() == reflect.Ptr || ft.Kind() == reflect.Slice { ft = ft.Elem() } @@ -215,11 +204,9 @@ func extractAssociationData(rt reflect.Type, index int) associationData { // Try to guess ref and fk if not defined. if ref == "" || fk == "" { // TODO: replace "id" with inferred primary field - if through != "" { + if assocData.through != "" { ref = "id" fk = "id" - refThrough = snaker.CamelToSnake(rt.Name()) + "_id" - fkThrough = snaker.CamelToSnake(ft.Name()) + "_id" } else if _, isBelongsTo := refDocData.index[fName+"_id"]; isBelongsTo { ref = fName + "_id" fk = "id" @@ -245,14 +232,7 @@ func extractAssociationData(rt reflect.Type, index int) associationData { // guess assoc type if sf.Type.Kind() == reflect.Slice || (sf.Type.Kind() == reflect.Ptr && sf.Type.Elem().Kind() == reflect.Slice) { - if through != "" { - assocData.typ = ManyToMany - assocData.referenceThrough = refThrough - assocData.foreignThrough = fkThrough - assocData.through = through - } else { - assocData.typ = HasMany - } + assocData.typ = HasMany } else { if len(assocData.referenceField) > len(assocData.foreignField) { assocData.typ = BelongsTo @@ -265,12 +245,3 @@ func extractAssociationData(rt reflect.Type, index int) associationData { return assocData } - -func getAssocField(tag reflect.StructTag, field string) (string, string) { - fields := strings.Split(tag.Get(field), ":") - if len(fields) == 2 { - return fields[0], fields[1] - } - - return fields[0], "" -} diff --git a/association_test.go b/association_test.go index 1b470a5b..45c6b888 100644 --- a/association_test.go +++ b/association_test.go @@ -197,68 +197,60 @@ func TestAssociation_Collection(t *testing.T) { foreignValue: nil, }, { - record: "User", - field: "Roles", - data: user, - typ: ManyToMany, - col: NewCollection(&user.Roles), - loaded: false, - isZero: true, - referenceField: "id", - referenceThrough: "user_id", - referenceValue: user.ID, - foreignField: "id", - foreignThrough: "role_id", - foreignValue: nil, - through: "user_roles", + record: "User", + field: "Roles", + data: user, + typ: HasMany, + col: NewCollection(&user.Roles), + loaded: false, + isZero: true, + referenceField: "id", + referenceValue: user.ID, + foreignField: "id", + foreignValue: nil, + through: "user_roles", }, { - record: "Role", - field: "Users", - data: role, - typ: ManyToMany, - col: NewCollection(&role.Users), - loaded: false, - isZero: true, - referenceField: "id", - referenceThrough: "role_id", - referenceValue: role.ID, - foreignField: "id", - foreignThrough: "user_id", - foreignValue: nil, - through: "user_roles", + record: "Role", + field: "Users", + data: role, + typ: HasMany, + col: NewCollection(&role.Users), + loaded: false, + isZero: true, + referenceField: "id", + referenceValue: role.ID, + foreignField: "id", + foreignValue: nil, + through: "user_roles", }, { - record: "User", - field: "Followers", - data: user, - typ: ManyToMany, - col: NewCollection(&user.Followers), - loaded: false, - isZero: true, - referenceField: "id", - referenceThrough: "following_id", - referenceValue: user.ID, - foreignField: "id", - foreignThrough: "follower_id", - foreignValue: nil, - through: "followers", + record: "User", + field: "Followers", + data: user, + typ: HasMany, + col: NewCollection(&user.Followers), + loaded: false, + isZero: true, + referenceField: "id", + referenceValue: user.ID, + foreignField: "id", + foreignValue: nil, + through: "followeds", }, { - record: "User", - field: "Followings", - data: user, - typ: ManyToMany, - col: NewCollection(&user.Followings), - loaded: false, - isZero: true, - referenceField: "id", - referenceThrough: "follower_id", - referenceValue: user.ID, - foreignField: "id", - foreignThrough: "following_id", - foreignValue: nil, - through: "followers", + record: "User", + field: "Followings", + data: user, + typ: HasMany, + col: NewCollection(&user.Followings), + loaded: false, + isZero: true, + referenceField: "id", + referenceValue: user.ID, + foreignField: "id", + foreignValue: nil, + through: "follows", }, } @@ -277,12 +269,10 @@ func TestAssociation_Collection(t *testing.T) { assert.Equal(t, test.isZero, assoc.IsZero()) assert.Equal(t, test.referenceField, assoc.ReferenceField()) assert.Equal(t, test.referenceValue, assoc.ReferenceValue()) - assert.Equal(t, test.referenceThrough, assoc.ReferenceThrough()) assert.Equal(t, test.foreignField, assoc.ForeignField()) - assert.Equal(t, test.foreignThrough, assoc.ForeignThrough()) assert.Equal(t, test.through, assoc.Through()) - if test.typ == HasMany || test.typ == ManyToMany { + if test.typ == HasMany { assert.Panics(t, func() { assert.Equal(t, test.foreignValue, assoc.ForeignValue()) }) @@ -295,6 +285,21 @@ func TestAssociation_Collection(t *testing.T) { } } +func TestAssociation_autosaveWithThrough(t *testing.T) { + type Alpha struct { + ID int + } + + type Beta struct { + ID int + Alpha Alpha `through:"other" autosave:"true"` + } + + assert.Panics(t, func() { + NewDocument(&Beta{}) + }) +} + func TestAssociation_refNotFound(t *testing.T) { type Alpha struct { ID int diff --git a/document_test.go b/document_test.go index 71f6818a..a1b29578 100644 --- a/document_test.go +++ b/document_test.go @@ -395,13 +395,13 @@ func TestDocument_Association(t *testing.T) { name: "User", record: &User{}, hasOne: []string{"address", "work_address"}, - hasMany: []string{"transactions", "user_roles", "emails"}, + hasMany: []string{"transactions", "user_roles", "emails", "roles", "follows", "followeds", "followings", "followers"}, }, { name: "User Cached", record: &User{}, hasOne: []string{"address", "work_address"}, - hasMany: []string{"transactions", "user_roles", "emails"}, + hasMany: []string{"transactions", "user_roles", "emails", "roles", "follows", "followeds", "followings", "followers"}, }, { name: "Transaction", diff --git a/rel_test.go b/rel_test.go index 6cc7f1ea..fe3385b5 100644 --- a/rel_test.go +++ b/rel_test.go @@ -20,15 +20,24 @@ type User struct { // user:id <- user_id:user_roles:role_id -> role:id Roles []Role `through:"user_roles"` - // many to many: self-referencing with explicitly defined ref and fk. - // omit mapped field. - Followers []User `ref:"id:following_id" fk:"id:follower_id" through:"followers"` - Followings []User `ref:"id:follower_id" fk:"id:following_id" through:"followers"` + // self-referencing needs two intermediate reference to be set up. + Follows []Follow `ref:"id" fk:"following_id"` + Followeds []Follow `ref:"id" fk:"follower_id"` + + // association through + Followings []User `through:"follows"` + Followers []User `through:"followeds"` CreatedAt time.Time UpdatedAt time.Time } +type Follow struct { + FollowerID int `db:",primary"` + FollowingID int `db:",primary"` + Accepted bool // this way, it may contains additional data +} + type Email struct { ID int Email string @@ -84,7 +93,7 @@ type Role struct { // explicit many to many declaration: // role:id <- role_id:user_roles:user_id -> user:id - Users []User `ref:"id:role_id" fk:"id:user_id" through:"user_roles"` + Users []User `through:"user_roles"` } type UserRole struct {