You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
183 lines
3.9 KiB
183 lines
3.9 KiB
package rel
|
|
|
|
import (
|
|
"reflect"
|
|
"sync"
|
|
|
|
"github.com/serenize/snaker"
|
|
)
|
|
|
|
var associationMetaCache sync.Map
|
|
|
|
type associationKey struct {
|
|
rt reflect.Type
|
|
// string repr of index, because []int is not hashable
|
|
index string
|
|
}
|
|
|
|
// AssociationType defines the type of association in database.
|
|
type AssociationType uint8
|
|
|
|
const (
|
|
// BelongsTo association.
|
|
BelongsTo = iota
|
|
// HasOne association.
|
|
HasOne
|
|
// HasMany association.
|
|
HasMany
|
|
)
|
|
|
|
type cachedAssociationMeta struct {
|
|
typ AssociationType
|
|
targetIndex []int
|
|
referenceField string
|
|
referenceIndex []int
|
|
foreignField string
|
|
foreignIndex []int
|
|
through string
|
|
autoload bool
|
|
autosave bool
|
|
}
|
|
|
|
type AssociationMeta struct {
|
|
rt reflect.Type
|
|
cachedAssociationMeta
|
|
}
|
|
|
|
// Type of association.
|
|
func (am AssociationMeta) Type() AssociationType {
|
|
return am.typ
|
|
}
|
|
|
|
// ReferenceField of the association.
|
|
func (am AssociationMeta) ReferenceField() string {
|
|
return am.referenceField
|
|
}
|
|
|
|
// ForeignField of the association.
|
|
func (am AssociationMeta) ForeignField() string {
|
|
return am.foreignField
|
|
}
|
|
|
|
// Through return intermediary association.
|
|
func (am AssociationMeta) Through() string {
|
|
return am.through
|
|
}
|
|
|
|
// Autoload assoc setting when parent is loaded.
|
|
func (am AssociationMeta) Autoload() bool {
|
|
return am.autoload
|
|
}
|
|
|
|
// Autosave setting when parent is created/updated/deleted.
|
|
func (am AssociationMeta) Autosave() bool {
|
|
return am.autosave
|
|
}
|
|
|
|
// Document returns association target document meta.
|
|
func (am AssociationMeta) DocumentMeta() DocumentMeta {
|
|
var (
|
|
rt = am.rt.FieldByIndex(am.targetIndex).Type
|
|
)
|
|
|
|
if rt.Kind() == reflect.Ptr {
|
|
rt = rt.Elem()
|
|
}
|
|
|
|
if rt.Kind() == reflect.Slice {
|
|
rt = rt.Elem()
|
|
}
|
|
|
|
return getDocumentMeta(rt, false)
|
|
}
|
|
|
|
func getAssociationMeta(rt reflect.Type, index []int) AssociationMeta {
|
|
var (
|
|
key = associationKey{
|
|
rt: rt,
|
|
index: encodeIndices(index),
|
|
}
|
|
)
|
|
|
|
if val, cached := associationMetaCache.Load(key); cached {
|
|
return AssociationMeta{
|
|
rt: rt,
|
|
cachedAssociationMeta: val.(cachedAssociationMeta),
|
|
}
|
|
}
|
|
|
|
var (
|
|
sf = rt.FieldByIndex(index)
|
|
ft = sf.Type
|
|
ref = sf.Tag.Get("ref")
|
|
fk = sf.Tag.Get("fk")
|
|
fName, _ = fieldName(sf)
|
|
assocMeta = cachedAssociationMeta{
|
|
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 assocMeta.autosave && assocMeta.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 (
|
|
refDocMeta = getDocumentMeta(rt, true)
|
|
fkDocMeta = getDocumentMeta(ft, true)
|
|
)
|
|
|
|
// Try to guess ref and fk if not defined.
|
|
if ref == "" || fk == "" {
|
|
// TODO: replace "id" with inferred primary field
|
|
if assocMeta.through != "" {
|
|
ref = "id"
|
|
fk = "id"
|
|
} else if _, isBelongsTo := refDocMeta.index[fName+"_id"]; isBelongsTo {
|
|
ref = fName + "_id"
|
|
fk = "id"
|
|
} else {
|
|
ref = "id"
|
|
fk = snaker.CamelToSnake(rt.Name()) + "_id"
|
|
}
|
|
}
|
|
|
|
if id, exist := refDocMeta.index[ref]; !exist {
|
|
panic("rel: references (" + ref + ") field not found ")
|
|
} else {
|
|
assocMeta.referenceIndex = id
|
|
assocMeta.referenceField = ref
|
|
}
|
|
|
|
if id, exist := fkDocMeta.index[fk]; !exist {
|
|
panic("rel: foreign_key (" + fk + ") field not found")
|
|
} else {
|
|
assocMeta.foreignIndex = id
|
|
assocMeta.foreignField = fk
|
|
}
|
|
|
|
// guess assoc type
|
|
if sf.Type.Kind() == reflect.Slice || (sf.Type.Kind() == reflect.Ptr && sf.Type.Elem().Kind() == reflect.Slice) {
|
|
assocMeta.typ = HasMany
|
|
} else {
|
|
if len(assocMeta.referenceField) > len(assocMeta.foreignField) {
|
|
assocMeta.typ = BelongsTo
|
|
} else {
|
|
assocMeta.typ = HasOne
|
|
}
|
|
}
|
|
|
|
associationMetaCache.Store(key, assocMeta)
|
|
|
|
return AssociationMeta{
|
|
rt: rt,
|
|
cachedAssociationMeta: assocMeta,
|
|
}
|
|
}
|