201 lines
6.3 KiB
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 {
|
|
|
|
}
|
|
}
|