diamond-orm/relationship.go

201 lines
6.3 KiB
Go

package orm
import (
"fmt"
sb "github.com/henvic/pgq"
"reflect"
"strings"
)
// RelationshipType - used to distinguish a Relationship between various
// common entity relationship types
type RelationshipType int
const (
HasOne RelationshipType = iota
HasMany
BelongsTo
ManyToOne // the other side of a HasMany relationship
ManyToMany
)
// Relationship - intermediate representation of how two types
// relate to each other. i.e., if struct A embeds struct B,
// a Relationship will be created for those two while parsing the Model for A.
type Relationship struct {
Type RelationshipType // the type of this relationship (see RelationshipType)
JoinTable string // the name of the join table, if specified explicitly via struct tag, otherwise blank
Model *Model // the primary Model which contains this relationship
FieldName string // the name of the struct field with this relationship
Idx int // the index of the struct field with this relationship
RelatedType reflect.Type // the reflect.Type for the struct field named by FieldName
RelatedModel *Model // the Model representing the type of the embedded slice/struct
Kind reflect.Kind // field kind (struct, slice, ...)
m2mInverse *Relationship // the "inverse" side of an explicit ManyToMany relationship
Nullable bool // whether the foreign key for this relationship can have a nullable column
OriginalField reflect.StructField // the original reflect.StructField object associated with this relationship
}
// ComputeJoinTable - computes the name of the join table for ManyToMany relationships.
// will return a snake_cased autogenerated name for unidirectional ManyToMany .
// the "implicit" behavior is invoked upon one of the following conditions being met:
// - the primary Relationship.Model
func (r *Relationship) ComputeJoinTable() string {
if r.JoinTable != "" {
return r.JoinTable
}
otherSide := r.RelatedModel.TableName
if r.Model.embeddedIsh {
otherSide = pascalToSnakeCase(r.FieldName)
}
return r.Model.TableName + "_" + otherSide
}
func (r *Relationship) relatedID() *Field {
return r.RelatedModel.Fields[r.RelatedModel.IDField]
}
func (r *Relationship) primaryID() *Field {
return r.Model.Fields[r.Model.IDField]
}
func (r *Relationship) joinField() string {
if r.Type == ManyToOne {
return r.RelatedModel.Name + "ID"
}
if r.Type == ManyToMany && !r.Model.embeddedIsh {
return r.RelatedModel.Name + "ID"
}
return r.FieldName + "ID"
}
func (r *Relationship) m2mIsh() bool {
needsMany := false
if !r.Model.embeddedIsh && r.RelatedModel.embeddedIsh {
rr, ok := r.RelatedModel.Relationships[r.Model.Name]
if ok && rr.Type != ManyToOne {
needsMany = true
}
}
return ((r.Model.embeddedIsh && !r.RelatedModel.embeddedIsh) || needsMany) &&
r.Type == HasMany
}
func (r *Relationship) joinInsert(v reflect.Value, e *Query, pfk any) error {
if r.Type != ManyToMany &&
!r.m2mIsh() {
return nil
}
ichild := v
for ichild.Kind() == reflect.Ptr {
ichild = ichild.Elem()
}
if ichild.Kind() == reflect.Struct {
jtable := r.ComputeJoinTable()
jargs := make([]any, 0)
jcols := make([]string, 0)
jcols = append(jcols, fmt.Sprintf("%s_id",
r.Model.TableName,
))
jargs = append(jargs, pfk)
jcols = append(jcols, r.RelatedModel.TableName+"_id")
jargs = append(jargs, ichild.FieldByName(r.RelatedModel.IDField).Interface())
var ecnt int
e.tx.QueryRow(e.ctx,
fmt.Sprintf("SELECT count(*) from %s where %s = $1 and %s = $2", r.ComputeJoinTable(), jcols[0], jcols[1]), jargs...).Scan(&ecnt)
if ecnt > 0 {
return nil
}
jsql := fmt.Sprintf("INSERT INTO %s (%s) VALUES ($1, $2)", jtable, strings.Join(jcols, ", "))
e.engine.logQuery("insert/join", jsql, jargs)
if !e.engine.dryRun {
_ = e.tx.QueryRow(e.ctx, jsql, jargs...).Scan()
}
}
return nil
}
func (r *Relationship) joinDelete(pk, fk any, q *Query) error {
dq := sb.Delete(r.ComputeJoinTable()).Where(fmt.Sprintf("%s_id = ?", r.Model.TableName), pk)
if fk != nil {
dq = dq.Where(fmt.Sprintf("%s_id = ?", r.RelatedModel.TableName), fk)
}
ds, aa := dq.MustSQL()
q.engine.logQuery("delete/join", ds, aa)
if !q.engine.dryRun {
_, err := q.tx.Exec(q.ctx, ds, aa...)
return err
}
return nil
}
func parseRelationship(field reflect.StructField, modelMap map[string]*Model, outerType reflect.Type, idx int, settings map[string]string) *Relationship {
rel := &Relationship{
Model: modelMap[outerType.Name()],
RelatedModel: modelMap[field.Type.Name()],
RelatedType: field.Type,
Idx: idx,
Kind: field.Type.Kind(),
FieldName: field.Name,
OriginalField: field,
}
if rel.RelatedType.Kind() == reflect.Slice || rel.RelatedType.Kind() == reflect.Array {
rel.RelatedType = rel.RelatedType.Elem()
}
if rel.RelatedModel == nil {
if rel.RelatedType.Name() == "" {
rt := rel.RelatedType
for rt.Kind() == reflect.Ptr || rt.Kind() == reflect.Slice || rt.Kind() == reflect.Array {
rel.Nullable = true
rel.RelatedType = rel.RelatedType.Elem()
rt = rel.RelatedType
}
}
rel.RelatedModel = modelMap[rel.RelatedType.Name()]
if _, ok := modelMap[rel.RelatedType.Name()]; !ok {
rel.RelatedModel = parseModel(reflect.New(rel.RelatedType).Interface())
modelMap[rel.RelatedType.Name()] = rel.RelatedModel
parseModelFields(rel.RelatedModel, modelMap)
rel.RelatedModel.embeddedIsh = true
}
}
switch field.Type.Kind() {
case reflect.Struct:
rel.Type = HasOne
case reflect.Slice, reflect.Array:
rel.Type = HasMany
}
maybeM2m := settings["m2m"]
if maybeM2m == "" {
maybeM2m = settings["manytomany"]
}
if rel.Type == HasMany && maybeM2m != "" {
rel.JoinTable = maybeM2m
}
return rel
}
func addForeignKeyFields(ref *Relationship) {
if !ref.RelatedModel.embeddedIsh && !ref.Model.embeddedIsh {
ref.Type = BelongsTo
} else if !ref.Model.embeddedIsh && ref.RelatedModel.embeddedIsh {
if ref.Type == HasMany {
nr := &Relationship{
RelatedModel: ref.Model,
Model: ref.RelatedModel,
Kind: ref.RelatedModel.Type.Kind(),
Idx: -1,
RelatedType: ref.Model.Type,
}
nr.Type = ManyToOne
nr.FieldName = nr.RelatedModel.Name
ref.RelatedModel.Relationships[nr.FieldName] = nr
} else if ref.Type == HasOne {
ref.Type = BelongsTo
}
} else if ref.Model.embeddedIsh && !ref.RelatedModel.embeddedIsh {
}
}