diamond-orm/relationship.go

192 lines
5.0 KiB
Go

package orm
import (
"fmt"
sb "github.com/henvic/pgq"
"reflect"
"strings"
)
type RelationshipType int
const (
HasOne RelationshipType = iota
HasMany
BelongsTo
ManyToOne
ManyToMany
)
type Relationship struct {
Type RelationshipType
JoinTable string
Model *Model
FieldName string
Idx int
RelatedType reflect.Type
RelatedModel *Model
Kind reflect.Kind // field kind (struct, slice, ...)
m2mInverse *Relationship
Nullable bool
OriginalField reflect.StructField
}
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()
fmt.Printf("[DELETE/JOIN] %s %+v \n", ds, logTrunc(200, 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 {
}
}