Skip to content

Commit beaaf34

Browse files
mclark4386stanislas-m
authored andcommitted
Allow existing models in eager/flat create (#199)
* Reset to development * Query in support with slice args. (#267) * [Fix] issue when using IN more than once '?' wildcards * slices with in * Fix some code style issues * Trim the Readme and add SHOULDERS (#268) * The lastest docs are on the website, and keeping them here mislead users. * Let's use SHOULDERS here too! * improved cockroach configuration and fixed bugs on connection details (#271) * improved cockroach configuration and fixed bugs on connection details * oops! didn't remove debugging log * bugfix: blank argument also affect as argument, in secure mode * Should fix not being able to use existing models in eager create associations * Should fix not being able to use existing models in eager create associations * added test to show what happens with partial update of associated object * added test to show what happens with partial update of associated object * *saving spot so I can rebase, but it's broken this commit... * *fixed bug and added a debug_test function to test script if needed * *linting and some copy paste I thought I fixed * *it may be done... going to improve testing to make sure it's good and solid * *best to put the comment in the right place if it's going to remind me properly... * *refactoring, bug fix, and better testing * *remove debug logging * *don't need that it would be doubling the call up since flat create handles it and that was called earlier * *improved docs and test coverage * *should fix requested changes (cleaned up the code a bit too) * *shouldn't be checking empty incorrectly anymore * *removed some un-needed code *added some docs *IsZeroOf now does a DeepEqual explictly * Reset to development * Query in support with slice args. (#267) * [Fix] issue when using IN more than once '?' wildcards * slices with in * Fix some code style issues * Trim the Readme and add SHOULDERS (#268) * The lastest docs are on the website, and keeping them here mislead users. * Let's use SHOULDERS here too! * improved cockroach configuration and fixed bugs on connection details (#271) * improved cockroach configuration and fixed bugs on connection details * oops! didn't remove debugging log * bugfix: blank argument also affect as argument, in secure mode * Deprecate Left and Right InnerJoins in favor of one InnerJoin. (#275) * Deprecate Left and Right InnerJoins in favor of one InnerJoin. * Add oncer Deprecation notices to LeftInnerJoin and RightInnerJoin * *should fix tests ('zero' UUIDs not getting seem as zero because they are converted to non-empty strings by ID()) * *updated update statement with more docs and to use sqlx.In * Improved tests * *couple of stupid ones * *removed leftover import from conflict resolve merge * *should fix checking an empty UUID in eagerCreate and validateAndOnlyCreate * *fix some merge mistakes and a fix * requested changes * Fix merge issue
1 parent c189364 commit beaaf34

14 files changed

+661
-30
lines changed

associations/association.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ type AssociationBeforeCreatable interface {
6868
type AssociationAfterCreatable interface {
6969
AfterInterface() interface{}
7070
AfterSetup() error
71+
AfterProcess() AssociationStatement
7172
Association
7273
}
7374

@@ -85,6 +86,11 @@ type AssociationStatement struct {
8586
Args []interface{}
8687
}
8788

89+
// Empty is true if the containing Statement is empty.
90+
func (as AssociationStatement) Empty() bool {
91+
return as.Statement == ""
92+
}
93+
8894
// Associations a group of model associations.
8995
type Associations []Association
9096

@@ -153,3 +159,8 @@ func isZero(i interface{}) bool {
153159
v := reflect.ValueOf(i)
154160
return v.Interface() == reflect.Zero(v.Type()).Interface()
155161
}
162+
163+
// IsZeroOfUnderlyingType will check if the value of anything is the equal to the Zero value of that type.
164+
func IsZeroOfUnderlyingType(x interface{}) bool {
165+
return reflect.DeepEqual(x, reflect.Zero(reflect.TypeOf(x)).Interface())
166+
}

associations/belongs_to_association.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,7 @@ func (b *belongsToAssociation) BeforeInterface() interface{} {
112112
return b.ownerModel.Interface()
113113
}
114114

115-
currentVal := b.ownerModel.Interface()
116-
zeroVal := reflect.Zero(b.ownerModel.Type()).Interface()
117-
if reflect.DeepEqual(zeroVal, currentVal) {
115+
if IsZeroOfUnderlyingType(b.ownerModel.Interface()) {
118116
return nil
119117
}
120118

associations/belongs_to_association_test.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ import (
44
"reflect"
55
"testing"
66

7-
"github.com/stretchr/testify/require"
8-
97
"github.com/gobuffalo/pop/associations"
10-
118
"github.com/gobuffalo/uuid"
9+
"github.com/stretchr/testify/require"
1210
)
1311

1412
type fooBelongsTo struct {
@@ -38,4 +36,17 @@ func Test_Belongs_To_Association(t *testing.T) {
3836
where, args := as[0].Constraint()
3937
a.Equal("id = ?", where)
4038
a.Equal(id, args[0].(uuid.UUID))
39+
40+
bar2 := barBelongsTo{FooID: uuid.Nil}
41+
as, err = associations.ForStruct(&bar2, "Foo")
42+
43+
a.NoError(err)
44+
a.Equal(len(as), 1)
45+
a.Equal(reflect.Struct, as[0].Kind())
46+
47+
before := as.AssociationsBeforeCreatable()
48+
49+
for index := range before {
50+
a.Equal(nil, before[index].BeforeInterface())
51+
}
4152
}

associations/has_many_association.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/gobuffalo/flect"
88
"github.com/gobuffalo/pop/nulls"
9+
"github.com/jmoiron/sqlx"
910
)
1011

1112
// hasManyAssociation is the implementation for the has_many
@@ -119,3 +120,51 @@ func (a *hasManyAssociation) AfterSetup() error {
119120
}
120121
return nil
121122
}
123+
124+
func (a *hasManyAssociation) AfterProcess() AssociationStatement {
125+
v := a.value
126+
if v.Kind() == reflect.Ptr {
127+
v = v.Elem()
128+
}
129+
130+
belongingIDFieldName := "ID"
131+
132+
ownerIDFieldName := "ID"
133+
ownerID := reflect.Indirect(reflect.ValueOf(a.owner)).FieldByName(ownerIDFieldName).Interface()
134+
135+
ids := []interface{}{}
136+
137+
for i := 0; i < v.Len(); i++ {
138+
id := v.Index(i).FieldByName(belongingIDFieldName).Interface()
139+
if !IsZeroOfUnderlyingType(id) {
140+
ids = append(ids, id)
141+
}
142+
}
143+
if len(ids) == 0 {
144+
return AssociationStatement{
145+
Statement: "",
146+
Args: []interface{}{},
147+
}
148+
}
149+
150+
fk := a.fkID
151+
if fk == "" {
152+
fk = flect.Underscore(a.ownerName) + "_id"
153+
}
154+
155+
// This will be used to update all of our owned models' foreign keys to our ID.
156+
ret := fmt.Sprintf("UPDATE %s SET %s = ? WHERE %s in (?);", a.tableName, fk, belongingIDFieldName)
157+
158+
update, args, err := sqlx.In(ret, ownerID, ids)
159+
if err != nil {
160+
return AssociationStatement{
161+
Statement: "",
162+
Args: []interface{}{},
163+
}
164+
}
165+
166+
return AssociationStatement{
167+
Statement: update,
168+
Args: args,
169+
}
170+
}

associations/has_one_association.go

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ import (
99
)
1010

1111
type hasOneAssociation struct {
12-
ownedModel reflect.Value
13-
ownedType reflect.Type
14-
ownerID interface{}
15-
ownerName string
16-
owner interface{}
17-
fkID string
12+
ownedTableName string
13+
ownedModel reflect.Value
14+
ownedType reflect.Type
15+
ownerID interface{}
16+
ownerName string
17+
owner interface{}
18+
fkID string
1819
*associationSkipable
1920
*associationComposite
2021
}
@@ -33,12 +34,13 @@ func hasOneAssociationBuilder(p associationParams) (Association, error) {
3334

3435
fval := p.modelValue.FieldByName(p.field.Name)
3536
return &hasOneAssociation{
36-
owner: p.model,
37-
ownedModel: fval,
38-
ownedType: fval.Type(),
39-
ownerID: ownerID.Interface(),
40-
ownerName: p.modelType.Name(),
41-
fkID: p.popTags.Find("fk_id").Value,
37+
owner: p.model,
38+
ownedTableName: flect.Pluralize(p.popTags.Find("has_one").Value),
39+
ownedModel: fval,
40+
ownedType: fval.Type(),
41+
ownerID: ownerID.Interface(),
42+
ownerName: p.modelType.Name(),
43+
fkID: p.popTags.Find("fk_id").Value,
4244
associationSkipable: &associationSkipable{
4345
skipped: skipped,
4446
},
@@ -76,9 +78,7 @@ func (h *hasOneAssociation) AfterInterface() interface{} {
7678
return h.ownedModel.Interface()
7779
}
7880

79-
currentVal := h.ownedModel.Interface()
80-
zeroVal := reflect.Zero(h.ownedModel.Type()).Interface()
81-
if reflect.DeepEqual(zeroVal, currentVal) {
81+
if IsZeroOfUnderlyingType(h.ownedModel.Interface()) {
8282
return nil
8383
}
8484

@@ -100,3 +100,33 @@ func (h *hasOneAssociation) AfterSetup() error {
100100

101101
return fmt.Errorf("could not set '%s' to '%s'", ownerID, fval)
102102
}
103+
104+
func (h *hasOneAssociation) AfterProcess() AssociationStatement {
105+
belongingIDFieldName := "ID"
106+
id := h.ownedModel.FieldByName(belongingIDFieldName).Interface()
107+
108+
ownerIDFieldName := "ID"
109+
ownerID := reflect.Indirect(reflect.ValueOf(h.owner)).FieldByName(ownerIDFieldName).Interface()
110+
111+
ids := []interface{}{ownerID}
112+
113+
if IsZeroOfUnderlyingType(id) {
114+
return AssociationStatement{
115+
Statement: "",
116+
Args: []interface{}{},
117+
}
118+
}
119+
ids = append(ids, id)
120+
121+
fk := h.fkID
122+
if fk == "" {
123+
fk = flect.Underscore(h.ownerName) + "_id"
124+
}
125+
126+
ret := fmt.Sprintf("UPDATE %s SET %s = ? WHERE %s = ?", h.ownedTableName, fk, belongingIDFieldName)
127+
128+
return AssociationStatement{
129+
Statement: ret,
130+
Args: ids,
131+
}
132+
}

associations/has_one_association_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ func Test_Has_One_Association(t *testing.T) {
3535
where, args := as[0].Constraint()
3636
a.Equal("foo_has_one_id = ?", where)
3737
a.Equal(id, args[0].(uuid.UUID))
38+
39+
foo2 := FooHasOne{}
40+
41+
as, err = associations.ForStruct(&foo2)
42+
after := as.AssociationsAfterCreatable()
43+
for index := range after {
44+
a.Equal(nil, after[index].AfterInterface())
45+
}
3846
}
3947

4048
func Test_Has_One_SetValue(t *testing.T) {

connection_details_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,10 @@ func Test_ConnectionDetails_Finalize_SQLite_with_Dialect(t *testing.T) {
164164
r.Equal("", cd.Port)
165165
r.Equal("", cd.User)
166166
}
167+
167168
func Test_ConnectionDetails_Finalize_SQLite_without_URL(t *testing.T) {
168169
r := require.New(t)
170+
169171
cd := &ConnectionDetails{
170172
Dialect: "sqlite",
171173
Database: "./foo.db",

dialect_cockroach.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"strings"
99
"sync"
1010

11-
_ "github.com/cockroachdb/cockroach-go/crdb" // Load CockroachdbQL/postgres Go driver. Also loads github.com/lib/pq
11+
_ "github.com/cockroachdb/cockroach-go/crdb" // Load CockroachdbQL/postgres Go driver which also loads github.com/lib/pq
1212
"github.com/gobuffalo/fizz"
1313
"github.com/gobuffalo/fizz/translators"
1414
"github.com/gobuffalo/pop/columns"
@@ -196,6 +196,7 @@ func (p *cockroach) Lock(fn func() error) error {
196196

197197
func (p *cockroach) DumpSchema(w io.Writer) error {
198198
cmd := exec.Command("cockroach", "dump", p.Details().Database, "--dump-mode=schema")
199+
199200
c := p.ConnectionDetails
200201
if defaults.String(c.Options["sslmode"], "disable") == "disable" || strings.Contains(c.RawOptions, "sslmode=disable") {
201202
cmd.Args = append(cmd.Args, "--insecure")

dialect_cockroach_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ func Test_Cockroach_URL_Raw(t *testing.T) {
1818
r.Equal("scheme://user:pass@host:port/database?option1=value1", m.URL())
1919
r.Equal("postgres://user:pass@host:port/?option1=value1", m.urlWithoutDb())
2020
}
21+
2122
func Test_Cockroach_URL_Build(t *testing.T) {
2223
r := require.New(t)
24+
2325
cd := &ConnectionDetails{
2426
Dialect: "cockroach",
2527
Database: "database",

executors.go

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package pop
22

33
import (
4-
"fmt"
4+
"reflect"
55

6+
"github.com/gobuffalo/pop/associations"
67
"github.com/gobuffalo/pop/columns"
78
"github.com/gobuffalo/pop/logging"
89
"github.com/gobuffalo/uuid"
@@ -60,13 +61,21 @@ func (c *Connection) ValidateAndSave(model interface{}, excludeColumns ...string
6061

6162
var emptyUUID = uuid.Nil.String()
6263

64+
// IsZeroOfUnderlyingType will check if the value of anything is the equal to the Zero value of that type.
65+
func IsZeroOfUnderlyingType(x interface{}) bool {
66+
return reflect.DeepEqual(x, reflect.Zero(reflect.TypeOf(x)).Interface())
67+
}
68+
6369
// Save wraps the Create and Update methods. It executes a Create if no ID is provided with the entry;
6470
// or issues an Update otherwise.
6571
func (c *Connection) Save(model interface{}, excludeColumns ...string) error {
6672
sm := &Model{Value: model}
6773
return sm.iterate(func(m *Model) error {
68-
id := m.ID()
69-
if fmt.Sprint(id) == "0" || fmt.Sprint(id) == emptyUUID {
74+
id, err := m.fieldByName("ID")
75+
if err != nil {
76+
return err
77+
}
78+
if IsZeroOfUnderlyingType(id.Interface()) {
7079
return c.Create(m.Value, excludeColumns...)
7180
}
7281
return c.Update(m.Value, excludeColumns...)
@@ -101,7 +110,11 @@ func (c *Connection) Create(model interface{}, excludeColumns ...string) error {
101110
sm := &Model{Value: model}
102111
return sm.iterate(func(m *Model) error {
103112
return c.timeFunc("Create", func() error {
104-
var err error
113+
asos, err := associations.ForStruct(m.Value)
114+
if err != nil {
115+
return err
116+
}
117+
105118
if err = m.beforeSave(c); err != nil {
106119
return err
107120
}
@@ -110,6 +123,23 @@ func (c *Connection) Create(model interface{}, excludeColumns ...string) error {
110123
return err
111124
}
112125

126+
processAssoc := len(asos) > 0
127+
128+
if processAssoc {
129+
before := asos.AssociationsBeforeCreatable()
130+
for index := range before {
131+
i := before[index].BeforeInterface()
132+
if i == nil {
133+
continue
134+
}
135+
136+
err = before[index].BeforeSetup()
137+
if err != nil {
138+
return err
139+
}
140+
}
141+
}
142+
113143
tn := m.TableName()
114144
cols := columns.ForStructWithAlias(m.Value, tn, m.As)
115145

@@ -124,6 +154,37 @@ func (c *Connection) Create(model interface{}, excludeColumns ...string) error {
124154
return err
125155
}
126156

157+
if processAssoc {
158+
after := asos.AssociationsAfterCreatable()
159+
for index := range after {
160+
stm := after[index].AfterProcess()
161+
if c.TX != nil && !stm.Empty() {
162+
_, err := c.TX.Exec(c.Dialect.TranslateSQL(stm.Statement), stm.Args...)
163+
if err != nil {
164+
return err
165+
}
166+
}
167+
}
168+
169+
stms := asos.AssociationsCreatableStatement()
170+
for index := range stms {
171+
statements := stms[index].Statements()
172+
for _, stm := range statements {
173+
if c.TX != nil {
174+
_, err := c.TX.Exec(c.Dialect.TranslateSQL(stm.Statement), stm.Args...)
175+
if err != nil {
176+
return err
177+
}
178+
continue
179+
}
180+
_, err = c.Store.Exec(c.Dialect.TranslateSQL(stm.Statement), stm.Args...)
181+
if err != nil {
182+
return err
183+
}
184+
}
185+
}
186+
}
187+
127188
if err = m.afterCreate(c); err != nil {
128189
return err
129190
}

0 commit comments

Comments
 (0)