Skip to content

Commit

Permalink
Auto populate join fields (#292)
Browse files Browse the repository at this point in the history
* extract metadata from Document and Association struct

* Automatically populate join query

* Fix join query

* add populator only if from and to is empty

* force migration v0.3.0

* go mod tidy

* Use new JoinAssoc api instead
  • Loading branch information
Fs02 committed Jun 26, 2022
1 parent f5e5856 commit 7edbcb0
Show file tree
Hide file tree
Showing 15 changed files with 887 additions and 583 deletions.
147 changes: 14 additions & 133 deletions association.go
Expand Up @@ -2,52 +2,17 @@ package rel

import (
"reflect"
"sync"

"github.com/serenize/snaker"
)

// AssociationType defines the type of association in database.
type AssociationType uint8

const (
// BelongsTo association.
BelongsTo = iota
// HasOne association.
HasOne
// HasMany association.
HasMany
)

type associationKey struct {
rt reflect.Type
// string repr of index, because []int is not hashable
index string
}

type associationData struct {
typ AssociationType
targetIndex []int
referenceField string
referenceIndex []int
foreignField string
foreignIndex []int
through string
autoload bool
autosave bool
}

var associationCache sync.Map

// Association provides abstraction to work with association of document or collection.
type Association struct {
data associationData
meta AssociationMeta
rv reflect.Value
}

// Type of association.
func (a Association) Type() AssociationType {
return a.data.typ
return a.meta.Type()
}

// Document returns association target as document.
Expand All @@ -65,7 +30,7 @@ func (a Association) LazyDocument() (*Document, bool) {

func (a Association) document(lazy bool) (*Document, bool) {
var (
rv = reflectValueFieldByIndex(a.rv, a.data.targetIndex, !lazy)
rv = reflectValueFieldByIndex(a.rv, a.meta.targetIndex, !lazy)
)

switch rv.Kind() {
Expand Down Expand Up @@ -96,7 +61,7 @@ func (a Association) document(lazy bool) (*Document, bool) {
// If association is zero, second return value will be false.
func (a Association) Collection() (*Collection, bool) {
var (
rv = reflectValueFieldByIndex(a.rv, a.data.targetIndex, true)
rv = reflectValueFieldByIndex(a.rv, a.meta.targetIndex, true)
loaded = !rv.IsNil()
)

Expand All @@ -119,25 +84,25 @@ func (a Association) Collection() (*Collection, bool) {
// IsZero returns true if association is not loaded.
func (a Association) IsZero() bool {
var (
rv = reflectValueFieldByIndex(a.rv, a.data.targetIndex, false)
rv = reflectValueFieldByIndex(a.rv, a.meta.targetIndex, false)
)

return isDeepZero(reflect.Indirect(rv), 1)
}

// ReferenceField of the association.
func (a Association) ReferenceField() string {
return a.data.referenceField
return a.meta.ReferenceField()
}

// ReferenceValue of the association.
func (a Association) ReferenceValue() interface{} {
return indirectInterface(reflectValueFieldByIndex(a.rv, a.data.referenceIndex, false))
return indirectInterface(reflectValueFieldByIndex(a.rv, a.meta.referenceIndex, false))
}

// ForeignField of the association.
func (a Association) ForeignField() string {
return a.data.foreignField
return a.meta.ForeignField()
}

// ForeignValue of the association.
Expand All @@ -148,29 +113,29 @@ func (a Association) ForeignValue() interface{} {
}

var (
rv = reflectValueFieldByIndex(a.rv, a.data.targetIndex, false)
rv = reflectValueFieldByIndex(a.rv, a.meta.targetIndex, false)
)

if rv.Kind() == reflect.Ptr {
rv = rv.Elem()
}

return indirectInterface(reflectValueFieldByIndex(rv, a.data.foreignIndex, false))
return indirectInterface(reflectValueFieldByIndex(rv, a.meta.foreignIndex, false))
}

// Through return intermediary association.
func (a Association) Through() string {
return a.data.through
return a.meta.Through()
}

// Autoload assoc setting when parent is loaded.
func (a Association) Autoload() bool {
return a.data.autoload
return a.meta.Autoload()
}

// Autosave setting when parent is created/updated/deleted.
func (a Association) Autosave() bool {
return a.data.autosave
return a.meta.Autosave()
}

func newAssociation(rv reflect.Value, index []int) Association {
Expand All @@ -179,91 +144,7 @@ func newAssociation(rv reflect.Value, index []int) Association {
}

return Association{
data: extractAssociationData(rv.Type(), index),
meta: getAssociationMeta(rv.Type(), index),
rv: rv,
}
}

func extractAssociationData(rt reflect.Type, index []int) associationData {
var (
key = associationKey{
rt: rt,
index: encodeIndices(index),
}
)

if val, cached := associationCache.Load(key); cached {
return val.(associationData)
}

var (
sf = rt.FieldByIndex(index)
ft = sf.Type
ref = sf.Tag.Get("ref")
fk = sf.Tag.Get("fk")
fName, _ = fieldName(sf)
assocData = associationData{
targetIndex: index,
through: sf.Tag.Get("through"),
autoload: sf.Tag.Get("auto") == "true" || sf.Tag.Get("autoload") == "true",
autosave: sf.Tag.Get("auto") == "true" || 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()
}

var (
refDocData = extractDocumentData(rt, true)
fkDocData = extractDocumentData(ft, true)
)

// Try to guess ref and fk if not defined.
if ref == "" || fk == "" {
// TODO: replace "id" with inferred primary field
if assocData.through != "" {
ref = "id"
fk = "id"
} else if _, isBelongsTo := refDocData.index[fName+"_id"]; isBelongsTo {
ref = fName + "_id"
fk = "id"
} else {
ref = "id"
fk = snaker.CamelToSnake(rt.Name()) + "_id"
}
}

if id, exist := refDocData.index[ref]; !exist {
panic("rel: references (" + ref + ") field not found ")
} else {
assocData.referenceIndex = id
assocData.referenceField = ref
}

if id, exist := fkDocData.index[fk]; !exist {
panic("rel: foreign_key (" + fk + ") field not found")
} else {
assocData.foreignIndex = id
assocData.foreignField = fk
}

// guess assoc type
if sf.Type.Kind() == reflect.Slice || (sf.Type.Kind() == reflect.Ptr && sf.Type.Elem().Kind() == reflect.Slice) {
assocData.typ = HasMany
} else {
if len(assocData.referenceField) > len(assocData.foreignField) {
assocData.typ = BelongsTo
} else {
assocData.typ = HasOne
}
}

associationCache.Store(key, assocData)

return assocData
}

0 comments on commit 7edbcb0

Please sign in to comment.