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

implement many to many relationships #183

Open
wants to merge 18 commits into
base: feature/m2m
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
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ test:
fi; \
done; \
go install ./generator/...; \
rm ./tests/kallax.go ; \
go generate ./tests/...; \
git diff --no-prefix -U1000; \
if [ `git status | grep 'Changes not staged for commit' | wc -l` != '0' ]; then \
Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ Imagine you have the following file in the package where your models are.
package models

type User struct {
kallax.Model `table:"users"`
ID kallax.ULID `pk:""`
kallax.Model `table:"users" pk:"id"`
ID kallax.ULID
Username string
Email string
Password string
Expand Down Expand Up @@ -90,7 +90,7 @@ Sometimes you might want to use the generated code in the same package it is def

A model is just a Go struct that embeds the `kallax.Model` type. All the fields of this struct will be columns in the database table.

A model also needs to have one (and just one) primary key. That is whatever field of the struct with the struct tag `pk`, which can be `pk:""` for a non auto-incrementable primary key or `pk:"autoincr"` for one that is auto-incrementable.
A model also needs to have one (and just one) primary key. The primary key is defined using the `pk` struct tag on the `kallax.Model` embedding. You can also set the primary key in a field of the struct with the struct tag `pk`, which can be `pk:""` for a non auto-incrementable primary key or `pk:"autoincr"` for one that is auto-incrementable.
More about primary keys is discussed at the [primary keys](#primary-keys) section.

First, let's review the rules and conventions for model fields:
Expand All @@ -112,9 +112,9 @@ Let's see an example of models with all these cases:

```go
type User struct {
kallax.Model `table:"users"`
kallax.Model `table:"users" pk:"id,autoincr"`
kallax.Timestamps
ID int64 `pk:"autoincr"`
ID int64
Username string
Password string
Emails []string
Expand Down Expand Up @@ -146,6 +146,8 @@ type Metadata struct {
| Tag | Description | Can be used in |
| --- | --- | --- |
| `table:"table_name"` | Specifies the name of the table for a model. If not provided, the name of the table will be the name of the struct in lower snake case (e.g. `UserPreference` => `user_preference`) | embedded `kallax.Model` |
| `pk:"primary_key_column_name"` | Specifies the column name of the primary key. | embedded `kallax.Model` |
| `pk:"primary_key_column_name,autoincr"` | Specifies the column name of the autoincrementable primary key. | embedded `kallax.Model` |
| `pk:""` | Specifies the field is a primary key | any field with a valid identifier type |
| `pk:"autoincr"` | Specifies the field is an auto-incrementable primary key | any field with a valid identifier type |
| `kallax:"column_name"` | Specifies the name of the column | Any model field that is not a relationship |
Expand Down Expand Up @@ -680,7 +682,7 @@ These are the flags available for `up` and `down`:
**Example:**

```
kallax migrate up --dir ./my-migrations --dns 'user:pass@localhost:5432/dbname?sslmode=disable' --version 1493991142
kallax migrate up --dir ./my-migrations --dsn 'user:pass@localhost:5432/dbname?sslmode=disable' --version 1493991142
```

### Type mappings
Expand Down
9 changes: 6 additions & 3 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
version: build-{build}.{branch}
platform: x64

image:
- Visual Studio 2015

clone_folder: c:\gopath\src\gopkg.in\src-d\go-kallax.v1

shallow_clone: true
shallow_clone: false

environment:
GOPATH: c:\gopath
Expand All @@ -16,12 +19,12 @@ services:
- postgresql96

install:
- set PATH=C:\Program Files\PostgreSQL\9.6\bin\;%GOPATH%\bin;c:\go\bin;%PATH%
- set PATH=C:\Program Files\PostgreSQL\9.6\bin\;C:\MinGW\bin;%GOPATH%\bin;c:\go\bin;%PATH%
- go version
- go get -v -t .\...

build: off

test_script:
- createdb testing
- go test -v .\...
- mingw32-make test
130 changes: 109 additions & 21 deletions batcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type batchQueryRunner struct {
q Query
oneToOneRels []Relationship
oneToManyRels []Relationship
throughRels []Relationship
db squirrel.DBProxy
builder squirrel.SelectBuilder
total int
Expand All @@ -29,6 +30,7 @@ func newBatchQueryRunner(schema Schema, db squirrel.DBProxy, q Query) *batchQuer
var (
oneToOneRels []Relationship
oneToManyRels []Relationship
throughRels []Relationship
)

for _, rel := range q.getRelationships() {
Expand All @@ -37,6 +39,8 @@ func newBatchQueryRunner(schema Schema, db squirrel.DBProxy, q Query) *batchQuer
oneToOneRels = append(oneToOneRels, rel)
case OneToMany:
oneToManyRels = append(oneToManyRels, rel)
case Through:
throughRels = append(throughRels, rel)
}
}

Expand All @@ -46,6 +50,7 @@ func newBatchQueryRunner(schema Schema, db squirrel.DBProxy, q Query) *batchQuer
q: q,
oneToOneRels: oneToOneRels,
oneToManyRels: oneToManyRels,
throughRels: throughRels,
db: db,
builder: builder,
}
Expand Down Expand Up @@ -125,8 +130,14 @@ func (r *batchQueryRunner) processBatch(rows *sql.Rows) ([]Record, error) {
return nil, err
}

if len(records) == 0 {
return nil, nil
}

var ids = make([]interface{}, len(records))
var identType Identifier
for i, r := range records {
identType = r.GetID()
ids[i] = r.GetID().Raw()
}

Expand All @@ -136,63 +147,140 @@ func (r *batchQueryRunner) processBatch(rows *sql.Rows) ([]Record, error) {
return nil, err
}

for _, r := range records {
err := r.SetRelationship(rel.Field, indexedResults[r.GetID().Raw()])
if err != nil {
return nil, err
}
err = setIndexedResults(records, rel, indexedResults)
if err != nil {
return nil, err
}
}

// If the relationship is partial, we can not ensure the results
// in the field reflect the truth of the database.
// In this case, the parent is marked as non-writable.
if rel.Filter != nil {
r.setWritable(false)
}
for _, rel := range r.throughRels {
indexedResults, err := r.getRecordThroughRelationships(ids, rel, identType)
if err != nil {
return nil, err
}

err = setIndexedResults(records, rel, indexedResults)
if err != nil {
return nil, err
}
}

return records, nil
}

func setIndexedResults(records []Record, rel Relationship, indexedResults indexedRecords) error {
for _, r := range records {
err := r.SetRelationship(rel.Field, indexedResults[r.GetID().Raw()])
if err != nil {
return err
}

// If the relationship is partial, we can not ensure the results
// in the field reflect the truth of the database.
// In this case, the parent is marked as non-writable.
if rel.Filter != nil {
r.setWritable(false)
}
}

return nil
}

type indexedRecords map[interface{}][]Record

func (r *batchQueryRunner) getRecordRelationships(ids []interface{}, rel Relationship) (indexedRecords, error) {
fk, ok := r.schema.ForeignKey(rel.Field)
if !ok {
return nil, fmt.Errorf("kallax: cannot find foreign key on field %s for table %s", rel.Field, r.schema.Table())
return nil, fmt.Errorf("kallax: cannot find foreign key on field %s of table %s", rel.Field, r.schema.Table())
}

filter := In(fk, ids...)
if rel.Filter != nil {
And(rel.Filter, filter)
} else {
rel.Filter = filter
filter = And(rel.Filter, filter)
}

q := NewBaseQuery(rel.Schema)
q.Where(filter)
cols, builder := q.compile()
rows, err := builder.RunWith(r.db).Query()
if err != nil {
return nil, err
}

return indexedResultsFromRows(rows, cols, rel.Schema, fk, nil)
}

func (r *batchQueryRunner) getRecordThroughRelationships(ids []interface{}, rel Relationship, identType Identifier) (indexedRecords, error) {
lfk, rfk, ok := r.schema.ForeignKeys(rel.Field)
if !ok {
return nil, fmt.Errorf("kallax: cannot find foreign keys for through relationship on field %s of table %s", rel.Field, r.schema.Table())
}

q := NewBaseQuery(rel.Schema)
q.Where(rel.Filter)
lschema := r.schema.WithAlias(rel.Schema.Alias())
intSchema := rel.IntermediateSchema.WithAlias(rel.Schema.Alias())
q.joinThrough(lschema, intSchema, rel.Schema, lfk, rfk)
q.where(In(lschema.ID(), ids...), lschema)
if rel.Filter != nil {
q.Where(rel.Filter)
}

if rel.IntermediateFilter != nil {
q.where(rel.IntermediateFilter, intSchema)
}
cols, builder := q.compile()
// manually add the extra column to also select the parent id
builder = builder.Column(lschema.ID().QualifiedName(lschema))
rows, err := builder.RunWith(r.db).Query()
if err != nil {
return nil, err
}

// we need to pass a new pointer of the parent identifier type so the
// resultset can fill it and we can know to which record it belongs when
// indexing by parent id.
return indexedResultsFromRows(rows, cols, rel.Schema, rfk, identType.newPtr())
}

// indexedResultsFromRows returns the results in the given rows indexed by the
// parent id. In the case of many to many relationships, the record odes not
// have a specific field with the ID of the parent to index by it,
// that's why parentIDPtr is passed for these cases. parentIDPtr is a pointer
// to an ID of the type required by the parent to be filled by the result set.
func indexedResultsFromRows(rows *sql.Rows, cols []string, schema Schema, fk SchemaField, parentIDPtr interface{}) (indexedRecords, error) {
relRs := NewResultSet(rows, false, nil, cols...)
var indexedResults = make(indexedRecords)
for relRs.Next() {
rec, err := relRs.Get(rel.Schema)
if err != nil {
return nil, err
var (
rec Record
err error
)

if parentIDPtr != nil {
rec, err = relRs.customGet(schema, parentIDPtr)
} else {
rec, err = relRs.Get(schema)
}

val, err := rec.Value(fk.String())
if err != nil {
return nil, err
}

rec.setPersisted()
rec.setWritable(true)
id := val.(Identifier).Raw()

var id interface{}
if parentIDPtr != nil {
id = parentIDPtr.(Identifier).Raw()
} else {
val, err := rec.Value(fk.String())
if err != nil {
return nil, err
}

id = val.(Identifier).Raw()
}

indexedResults[id] = append(indexedResults[id], rec)
}

Expand Down
Loading