Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Go backlinks support #38

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions internal/generator/binding/backlink.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2020 ObjectBox Ltd. All rights reserved.
* https://objectbox.io
*
* This file is part of ObjectBox Generator.
*
* ObjectBox Generator is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* ObjectBox Generator is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ObjectBox Generator. If not, see <http://www.gnu.org/licenses/>.
*/

package binding

// Backlink describes a relation backlink. Backlinks aren't stored in the model JSON.
type Backlink struct {
SourceEntity string
SourceProperty string
}

// RelatedEntityName gets the relation source entity name
func (relation *Backlink) RelatedEntityName() string {
return relation.SourceEntity
}
1 change: 1 addition & 0 deletions internal/generator/binding/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ func (object *Object) AddRelation(details map[string]*Annotation) (*model.Standa
return nil, fmt.Errorf("name annotation value must not be empty on relation - it's the relation name in DB")
}
relation.Name = details["name"].Value
relation.IsLazyLoaded = details["lazy"] != nil

if details["to"] == nil || len(details["to"].Value) == 0 {
return nil, fmt.Errorf("to annotation value must not be empty on relation %s - specify target entity", relation.Name)
Expand Down
3 changes: 2 additions & 1 deletion internal/generator/c/cgenerator.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ func (gen *CGenerator) generateBindingFile(bindingFile, headerFile string, m *mo
GeneratorVersion int
FileIdentifier string
HeaderFile string
}{m, generator.VersionId, fileIdentifier, filepath.Base(headerFile)}
Optional string
}{m, generator.VersionId, fileIdentifier, filepath.Base(headerFile), gen.Optional}

var tpl *template.Template

Expand Down
5 changes: 5 additions & 0 deletions internal/generator/c/templates/binding-hpp.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ var CppBindingTemplateHeader = template.Must(template.New("binding-hpp").Funcs(f

#include <cstdbool>
#include <cstdint>
{{- if eq "std::optional" .Optional}}
#include <optional>
{{- else if .Optional}}
#include <memory>
{{end}}

#include "flatbuffers/flatbuffers.h"
#include "objectbox.h"
Expand Down
8 changes: 4 additions & 4 deletions internal/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,14 +166,14 @@ func createBinding(options Options, storedModel *model.ModelInfo) error {
return fmt.Errorf("model finalization failed: %s", err)
}

if err = options.CodeGenerator.WriteBindingFiles(filePath, options, storedModel); err != nil {
return err
}

for _, entity := range storedModel.EntitiesWithMeta() {
entity.CurrentlyPresent = true
}

if err = options.CodeGenerator.WriteBindingFiles(filePath, options, storedModel); err != nil {
return err
}

return nil
})
}
Expand Down
58 changes: 40 additions & 18 deletions internal/generator/go/ast-reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ var supportedEntityAnnotations = map[string]bool{

var supportedPropertyAnnotations = map[string]bool{
"-": true,
"backlink": true,
"converter": true,
"date": true,
"date-nano": true,
Expand Down Expand Up @@ -122,7 +123,7 @@ type Field struct {
Property *Property // nil if it's an embedded struct
Fields []*Field // inner fields, nil if it's a property
StandaloneRelation *model.StandaloneRelation // to-many relation stored as a standalone relation in the model
IsLazyLoaded bool // only standalone (to-many) relations currently support lazy loading
Backlink *binding.Backlink // non-null if this field describes a relation backlink
Meta *Field // self reference for recursive ".Meta.Fields" access in the template

path string // relative addressing path for embedded structs
Expand Down Expand Up @@ -262,7 +263,7 @@ func (entity *Entity) addFields(parent *Field, fields fieldList, fieldPath, pref
log.Printf("%s property %s found in %s", text, property.Name, fieldPath)
}
var propertyError = func(err error, property *Property) error {
return fmt.Errorf("%s on property %s found in %s", err, property.Name, fieldPath)
return fmt.Errorf("property %s.%s: %s", fieldPath, property.Name, err)
}

var children []*Field
Expand Down Expand Up @@ -499,24 +500,35 @@ func (field *Field) processType(f field) (fields fieldList, err error) {
if slice, isSlice := baseType.(*types.Slice); isSlice {
var elementType = slice.Elem()

// it's a many-to-many relation
if err := property.setRelationAnnotation(typeBaseName(elementType.String()), true); err != nil {
return nil, err
}
relatedEntity := typeBaseName(elementType.String())

var relDetails = make(map[string]*binding.Annotation)
relDetails["name"] = &binding.Annotation{Value: field.Name}
relDetails["to"] = property.annotations["link"]
relDetails["uid"] = property.annotations["uid"]
if rel, err := field.Entity.AddRelation(relDetails); err != nil {
return nil, err
// if it's a backlink (the source may be either a to-one relation or a to-many relation)
if property.annotations["backlink"] != nil {
if property.annotations["link"] != nil {
return nil, fmt.Errorf("cannot combine 'link' and 'backlink' annotations on a single field")
}

field.Backlink = &binding.Backlink{
SourceEntity: relatedEntity,
SourceProperty: property.annotations["backlink"].Value,
}
property.IsBasicType = false // override the value set by setBasicType
} else {
field.StandaloneRelation = rel
}
// it's a many-to-many relation
if err := property.setRelationAnnotation(typeBaseName(elementType.String()), true); err != nil {
return nil, err
}

if field.Property.annotations["lazy"] != nil {
// relations only
field.IsLazyLoaded = true
var relDetails = make(map[string]*binding.Annotation)
relDetails["name"] = &binding.Annotation{Value: field.Name}
relDetails["to"] = property.annotations["link"]
relDetails["uid"] = property.annotations["uid"]
relDetails["lazy"] = property.annotations["lazy"]
if rel, err := field.Entity.AddRelation(relDetails); err != nil {
return nil, err
} else {
field.StandaloneRelation = rel
}
}

// fill in the field information
Expand Down Expand Up @@ -772,6 +784,16 @@ func (entity *Entity) HasLazyLoadedRelations() bool {
return false
}

// ToMany called from the template.
func (field *Field) ToMany() interface{} {
if field.StandaloneRelation != nil {
return field.StandaloneRelation
} else if field.Backlink != nil {
return field.Backlink
}
return nil
}

// HasRelations called from the template.
func (field *Field) HasRelations() bool {
if field.StandaloneRelation != nil || len(field.Property.ModelProperty.RelationTarget) > 0 {
Expand All @@ -789,7 +811,7 @@ func (field *Field) HasRelations() bool {

// HasLazyLoadedRelations called from the template.
func (field *Field) HasLazyLoadedRelations() bool {
if field.StandaloneRelation != nil && field.IsLazyLoaded {
if field.StandaloneRelation != nil && field.StandaloneRelation.IsLazyLoaded {
return true
}

Expand Down
37 changes: 35 additions & 2 deletions internal/generator/go/gogenerator.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,43 @@ func (goGen *GoGenerator) ParseSource(sourceFile string) (*model.ModelInfo, erro
return goGen.binding.model, nil
}

// Update backlink specifications (source property name)
func resolveBacklinks(mergedModel *model.ModelInfo) error {
for _, entity := range mergedModel.Entities {
if !entity.CurrentlyPresent {
continue
}
for _, field := range entity.Meta.(*Entity).Fields {
if field.Backlink == nil || len(field.Backlink.SourceProperty) != 0 {
continue
}
if srcEntity, err := mergedModel.FindEntityByName(field.Backlink.SourceEntity); err != nil {
return fmt.Errorf("backlink %v.%v source lookup failed: %v", entity.Name, field.Name, err)
} else {
for _, prop := range srcEntity.Properties {
if prop.RelationTarget == entity.Name {
if len(field.Backlink.SourceProperty) == 0 {
field.Backlink.SourceProperty = prop.Name
} else {
return fmt.Errorf("backlink %v.%v matches multiple source properties: %v and %v",
entity.Name, field.Name, field.Backlink.SourceProperty, prop.Name)
}
}
}
}
}
}
return nil
}

func (goGen *GoGenerator) WriteBindingFiles(sourceFile string, options generator.Options, mergedModel *model.ModelInfo) error {
// NOTE: find a better place for this check - we only want to do it for some languages
// should be called after generator calls storedMode.Finalize()
if err := mergedModel.CheckRelationCycles(); err != nil {
if err := model.CheckRelationCycles(mergedModel.Entities...); err != nil {
return err
}

if err := resolveBacklinks(mergedModel); err != nil {
return err
}

Expand Down Expand Up @@ -154,7 +187,7 @@ func (goGen *GoGenerator) WriteModelBindingFile(options generator.Options, model
}

if formattedSource, err := format.Source(modelSource); err != nil {
// we just store error but still writ the file so that we can check it manually
// we just store error but still write the file so that we can check it manually
err2 = fmt.Errorf("failed to format generated model file %s: %s", modelFile, err)
} else {
modelSource = formattedSource
Expand Down
90 changes: 52 additions & 38 deletions internal/generator/go/templates/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,46 @@ var BindingTemplate = template.Must(template.New("binding").Funcs(funcMap).Parse
{{- else}}{{if .GoField.IsPointer}}*{{end}}obj.{{.Path}}{{end}}
{{- end -}}

{{define "fetch-toMany"}}{{/* used in fetch-related for standalone relations and backlinks*/ -}}
// It will "GetManyExisting()" all related {{.ToMany.RelatedEntityName}} objects for each source object
// and set sourceObject.{{.Name}} to the slice of related objects, as currently stored in DB.
func (box *{{.Entity.Name}}Box) Fetch{{.Name}}(sourceObjects ...*{{.Entity.Name}}) error {
var slices = make([]{{.Type}}, len(sourceObjects))
err := box.ObjectBox.RunInReadTx(func() error {
// collect slices before setting the source objects' fields
// this keeps all the sourceObjects untouched in case there's an error during any of the requests
for k, object := range sourceObjects {
{{if .Entity.ModelEntity.IdProperty.Meta.Converter -}}
sourceId, err := {{.Entity.ModelEntity.IdProperty.Meta.Converter}}ToDatabaseValue(object.{{.Entity.ModelEntity.IdProperty.Meta.Path}})
if err != nil {
return err
}
{{end -}}
{{if .Backlink -}}
rIds, err := BoxFor{{.ToMany.RelatedEntityName}}(box.ObjectBox).RelationBackLinkIds({{.Backlink.RelatedEntityName}}_.{{.Backlink.SourceProperty}},
{{- else -}}
rIds, err := box.RelationIds({{.Entity.Name}}_.{{.Name}},
{{- end -}}
{{with .Entity.ModelEntity.IdProperty}} {{if .Meta.Converter}}sourceId{{else}}object.{{.Meta.Path}}{{end}}{{end}})
if err == nil {
slices[k], err = BoxFor{{.ToMany.RelatedEntityName}}(box.ObjectBox).GetManyExisting(rIds...)
}
if err != nil {
return err
}
}
return nil
})

if err == nil { // update the field on all objects if we got all slices
for k := range sourceObjects {
sourceObjects[k].{{.Name}} = slices[k]
}
}
return err
}
{{- end -}}


package {{.Binding.Package.Name}}

Expand Down Expand Up @@ -165,11 +205,11 @@ func ({{$entityNameCamel}}_EntityInfo) PutRelated(ob *objectbox.ObjectBox, objec
{{- block "put-relations" $entity}}
{{- range $field := .Meta.Fields}}
{{- if $field.StandaloneRelation}}
{{- if $field.IsLazyLoaded}} if object.(*{{$field.Entity.Name}}).{{$field.Path}} != nil { // lazy-loaded relations without {{$field.Entity.Name}}Box::Fetch{{$field.Name}}() called are nil {{end}}
{{- if $field.StandaloneRelation.IsLazyLoaded}} if object.(*{{$field.Entity.Name}}).{{$field.Path}} != nil { // lazy-loaded relations without {{$field.Entity.Name}}Box::Fetch{{$field.Name}}() called are nil {{end}}
if err := BoxFor{{$field.Entity.Name}}(ob).RelationReplace({{.Entity.Name}}_.{{$field.Name}}, id, object, object.(*{{$field.Entity.Name}}).{{$field.Path}}); err != nil {
return err
}
{{if $field.IsLazyLoaded}} } {{end}}
{{if $field.StandaloneRelation.IsLazyLoaded}} } {{end}}
{{- else if $field.Property}}
{{- if and (not $field.Property.IsBasicType) $field.Property.ModelProperty.RelationTarget}}
if rel := {{if not $field.IsPointer}}&{{end}}object.(*{{$field.Entity.Name}}).{{$field.Path}}; rel != nil {
Expand Down Expand Up @@ -296,7 +336,7 @@ func ({{$entityNameCamel}}_EntityInfo) Load(ob *objectbox.ObjectBox, bytes []byt
{{- block "load-relations" $entity}}
{{- range $field := .Meta.Fields}}
{{if $field.StandaloneRelation -}}
{{if not $field.IsLazyLoaded -}}
{{if not $field.StandaloneRelation.IsLazyLoaded -}}
var rel{{$field.Name}} {{$field.Type}}
if rIds, err := BoxFor{{$field.Entity.Name}}(ob).RelationIds({{.Entity.Name}}_.{{$field.Name}}, prop{{.Entity.ModelEntity.IdProperty.Name}}); err != nil {
return nil, err
Expand Down Expand Up @@ -333,8 +373,10 @@ func ({{$entityNameCamel}}_EntityInfo) Load(ob *objectbox.ObjectBox, bytes []byt
{{- block "fields-initializer" $entity}}
{{- range $field := .Meta.Fields}}
{{$field.Name}}:
{{- if $field.StandaloneRelation}}
{{- if $field.IsLazyLoaded}}nil, // use {{$field.Entity.Name}}Box::Fetch{{$field.Name}}() to fetch this lazy-loaded relation
{{- if $field.Backlink}}
nil, // use {{$field.Entity.Name}}Box::Fetch{{$field.Name}}() to fetch this lazy-loaded relation backlink
{{- else if $field.StandaloneRelation}}
{{- if $field.StandaloneRelation.IsLazyLoaded}}nil, // use {{$field.Entity.Name}}Box::Fetch{{$field.Name}}() to fetch this lazy-loaded relation
{{- else}}rel{{$field.Name}}
{{- end}}
{{- else if $field.Property}}
Expand Down Expand Up @@ -458,41 +500,13 @@ func (box *{{$entity.Name}}Box) GetAll() ([]{{if not $.ByValue}}*{{end}}{{$entit
{{- block "fetch-related" $entity}}
{{- range $field := .Meta.Fields}}
{{if .StandaloneRelation}}
{{- if .IsLazyLoaded -}}
{{- if .StandaloneRelation.IsLazyLoaded -}}
// Fetch{{.Name}} reads target objects for relation {{.Entity.Name}}::{{.Name}}.
// It will "GetManyExisting()" all related {{.StandaloneRelation.Target.Name}} objects for each source object
// and set sourceObject.{{.Name}} to the slice of related objects, as currently stored in DB.
func (box *{{.Entity.Name}}Box) Fetch{{.Name}}(sourceObjects ...*{{.Entity.Name}}) error {
var slices = make([]{{.Type}}, len(sourceObjects))
err := box.ObjectBox.RunInReadTx(func() error {
// collect slices before setting the source objects' fields
// this keeps all the sourceObjects untouched in case there's an error during any of the requests
for k, object := range sourceObjects {
{{if .Entity.ModelEntity.IdProperty.Meta.Converter -}}
sourceId, err := {{.Entity.ModelEntity.IdProperty.Meta.Converter}}ToDatabaseValue(object.{{.Entity.ModelEntity.IdProperty.Meta.Path}})
if err != nil {
return err
}
{{end -}}
rIds, err := box.RelationIds({{.Entity.Name}}_.{{.Name}}, {{with .Entity.ModelEntity.IdProperty}} {{if .Meta.Converter}}sourceId{{else}}object.{{.Meta.Path}}{{end}}{{end}})
if err == nil {
slices[k], err = BoxFor{{.StandaloneRelation.Target.Name}}(box.ObjectBox).GetManyExisting(rIds...)
}
if err != nil {
return err
}
}
return nil
})

if err == nil { // update the field on all objects if we got all slices
for k := range sourceObjects {
sourceObjects[k].{{.Name}} = slices[k]
}
}
return err
}
{{template "fetch-toMany" $field}}
{{end}}
{{- else if .Backlink}}
// Fetch{{.Name}} reads source objects back-linked by relation {{.Backlink.RelatedEntityName}}::{{.Backlink.SourceProperty}}.
{{template "fetch-toMany" $field}}
{{- else if not .Property}}{{/* recursively visit fields in embedded structs */}}{{template "fetch-related" $field}}
{{- end}}
{{- end}}{{end}}
Expand Down
3 changes: 2 additions & 1 deletion internal/generator/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ func getModelRelation(currentRelation *model.StandaloneRelation, storedEntity *m

func mergeModelRelation(currentRelation *model.StandaloneRelation, storedRelation *model.StandaloneRelation, storedModel *model.ModelInfo) (err error) {
storedRelation.Name = currentRelation.Name
storedRelation.IsLazyLoaded = currentRelation.IsLazyLoaded

if currentRelation.Meta != nil {
storedRelation.Meta = currentRelation.Meta.Merge(storedRelation)
Expand All @@ -334,7 +335,7 @@ func mergeModelRelation(currentRelation *model.StandaloneRelation, storedRelatio
currentRelation.Id = storedRelation.Id
}

// find the target entity & read it's ID/UID for the binding code
// find the target entity & read its ID/UID for the binding code
if targetEntity, err := storedModel.FindEntityByName(currentRelation.Target.Name); err != nil {
return err
} else if _, _, err = targetEntity.Id.Get(); err != nil {
Expand Down
Loading