Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Has Through Schema #128

Merged
merged 1 commit into from
Oct 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
77 changes: 24 additions & 53 deletions association.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package rel

import (
"reflect"
"strings"
"sync"

"github.com/serenize/snaker"
Expand All @@ -18,8 +17,6 @@ const (
HasOne
// HasMany association.
HasMany
// ManyToMany association.
ManyToMany
)

type associationKey struct {
Expand All @@ -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
Expand Down Expand Up @@ -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))
Expand All @@ -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 (
Expand All @@ -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
}
Expand Down Expand Up @@ -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()
}
Expand All @@ -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"
Expand All @@ -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
Expand All @@ -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], ""
}
123 changes: 64 additions & 59 deletions association_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
}

Expand All @@ -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())
})
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
19 changes: 14 additions & 5 deletions rel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down