Skip to content

Commit

Permalink
Pop Eager Creation (#14)
Browse files Browse the repository at this point in the history
* add slices and array support for create, update, save, validateAndCreate, validateAndSave, validateAndUpdate

* change return reference on Eager function from *Query To *Connection

* add has_many eager creation feature

* add has one association support for eager creation

* add belongs_to eager creation feature

* many to many association support

* add SQL translator to accomplish dialect params

* extract skipped function into skipable struct

* add AssociationCreatable interface to define associations that can be created

* Update README.md

* add exclude columns for eager model creation

* Fix broken test

* Update README.md

* add support for UUID generation before create a many to many relationship

* [Fix] allow eager creation when there are not associations defined for model

* remove assoc package dependency

* add owner eligible for creation flag to belongs_to associations

* Replace design for eager creation by using Before and After type associations

* Fix Broken Test

* Use store instead of TX when it has a nil reference

* clear slices when eager loading from many_to_many and has_many associations

* [Refactor] remove duplicated code for iterations over models

* [Refactor] remove duplicated code and improve validations type

* [Refactor] remove unused code
  • Loading branch information
larrymjordan authored and markbates committed Apr 16, 2018
1 parent 8745d21 commit 4f62567
Show file tree
Hide file tree
Showing 20 changed files with 1,204 additions and 181 deletions.
122 changes: 121 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ user := models.User{}
err := tx.Find(&user, id)
```

#### Query
#### Query All
```go
tx := models.DB
query := tx.Where("id = 1").Where("name = 'Mark'")
Expand Down Expand Up @@ -289,6 +289,80 @@ sql, args := query.ToSQL(&pop.Model{Value: models.UserRole{}}, "user_roles.*",
err := models.DB.RawQuery(sql, args...).All(&roles)
```

#### Create
```go
// Create one record.
user := models.User{}
user.Name = "Mark"
err := tx.Create(&user)

// Create many records.
users := models.Users{
{Name:"Mark"},
{Name: "Larry"},
}

err := tx.Create(&users)
```

#### Save
```go
// Save one record.
user := models.User{}
user.Name = "Mark"
err := tx.Save(&user)

// Save many records.
users := models.Users{
{Name:"Mark"},
{Name: "Larry"},
}

err := tx.Save(&users)
```

#### Update
```go
// Update one record.
user := models.User{}
user.Name = "Mark"
err := tx.Create(&user)

user.Name = "Mark Bates"
err = tx.Update(&user)

// Update many records.
users := models.Users{
{Name:"Mark"},
{Name: "Larry"},
}

err := tx.Create(&users)

users[0].Name = "Mark Bates"
users[1].Name = "Larry Morales"
err := tx.Update(&users)
```

#### Destroy
```go
// Destroy one record.
user := models.User{}
user.Name = "Mark"
err := tx.Create(&user)

err = tx.Destroy(&user)

// Destroy many records.
users := models.Users{
{Name:"Mark"},
{Name: "Larry"},
}
err := tx.Create(&users)

err = tx.Destroy(&users)
```

### Eager Loading
**pop** allows you to perform an eager loading for associations defined in a model. By using `pop.Connection.Eager()` function plus some fields tags predefined in your model you can extract associated data from a model.

Expand Down Expand Up @@ -375,6 +449,52 @@ tx.Eager("Books.Writers.Book").First(&u) // will load all Books for u and for e
tx.Eager("Books.Writers").Eager("FavoriteSong").First(&u) // will load all Books for u and for every Book will load all Writers. And Also it will load the favorite song for user.
```

#### Eager Creation
pop allows you to eager create models and their associations in just one simple statement, you don't need to create every association separately anymore.
```go
user := User{
Name: "Mark Bates",
Books: Books{{Title: "Pop Book", Description: "Pop Book", Isbn: "PB1"}},
FavoriteSong: Song{Title: "Don't know the title"},
Houses: Addresses{
Address{HouseNumber: 1, Street: "Golang"},
},
}
```
```go
err := tx.Eager().Create(&user)
```
The above sentence will do this:
1. It will notice `Books` is a `has_many` association and it will realize that to actually store every book it will need to get the `User ID` first. So, it proceeds to store first `User` data so it can retrieve an **ID** and then use that ID to fill `UserID` field in every `Book` in `Books`. Later it stores all books in database.
2. `FavoriteSong` is a `has_one` association and it uses same logic described in `has_many` association. Since `User` data was previously saved before creating all books, it already knows that `User` got an `ID` so it fills its `UserID` field with that value and `FavoriteSong` is then stored in database.
3. `Houses` for this example is a `many_to_many` relationship and it will have to deal with two tables in this case: `users` and `addresses`. It will need to store all addresses first in `addresses` table before save them in the many to many table. Because `User` was already stored, it already have an `ID`. This is a special case to deal with, since this behavior is different to all other associations, it managed to solve it by let it implement the `AssociationCreatableStatement` interface, all other associations implement by default `AssociationCreatable` interface.
For `belongs_to` association like shown in the example bellow, it will need first to create `User` to retrieve **ID** value and then fill its `UserID` field before be saved in database.
```go
book := Book{
Title: "Pop Book",
Description: "Pop Book",
Isbn: "PB1",
User: User{
Name: nulls.NewString("Larry"),
},
}
```
```go
tx.Eager().Create(&book)
```
All these cases are assuming that none of models and associations has previously been saved in database.
#### Callbacks
Pop provides a means to execute code before and after database operations.
This is done by defining specific methods on your models. For
Expand Down
95 changes: 80 additions & 15 deletions associations/association.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"reflect"

"github.com/gobuffalo/pop/columns"
"github.com/gobuffalo/pop/nulls"
)

// Association represents a definition of a model association
Expand All @@ -14,6 +15,17 @@ type Association interface {
Interface() interface{}
Constraint() (string, []interface{})
InnerAssociations() InnerAssociations
Skipped() bool
}

// associationSkipable is a helper struct that helps
// to include skippable behavior in associations.
type associationSkipable struct {
skipped bool
}

func (a *associationSkipable) Skipped() bool {
return a.skipped
}

// associationComposite adds the ability for a Association to
Expand Down Expand Up @@ -43,12 +55,70 @@ type AssociationSortable interface {
Association
}

type AssociationBeforeCreatable interface {
BeforeInterface() interface{}
BeforeSetup() error
Association
}

type AssociationAfterCreatable interface {
AfterInterface() interface{}
AfterSetup() error
Association
}

// AssociationCreatableStatement a association that defines
// create statements on database.
type AssociationCreatableStatement interface {
Statements() []AssociationStatement
Association
}

// AssociationStatement a type that represents a statement to be
// executed.
type AssociationStatement struct {
Statement string
Args []interface{}
}

// Associations a group of model associations.
type Associations []Association

// SkippedAssociation an empty association used to indicate
// an association should not be queried.
var SkippedAssociation = (Association)(nil)
// AssociationsBeforeCreatable returns all associations that implement AssociationBeforeCreatable
// interface. Belongs To association is an example of this implementation.
func (a Associations) AssociationsBeforeCreatable() []AssociationBeforeCreatable {
before := []AssociationBeforeCreatable{}
for i := range a {
if _, ok := a[i].(AssociationBeforeCreatable); ok {
before = append(before, a[i].(AssociationBeforeCreatable))
}
}
return before
}

// AssociationsAfterCreatable returns all associations that implement AssociationAfterCreatable
// interface. Has Many and Has One associations are example of this implementation.
func (a Associations) AssociationsAfterCreatable() []AssociationAfterCreatable {
after := []AssociationAfterCreatable{}
for i := range a {
if _, ok := a[i].(AssociationAfterCreatable); ok {
after = append(after, a[i].(AssociationAfterCreatable))
}
}
return after
}

// AssociationsCreatableStatement returns all associations that implement AssociationCreatableStament
// interface. Many To Many association is an example of this implementation.
func (a Associations) AssociationsCreatableStatement() []AssociationCreatableStatement {
stm := []AssociationCreatableStatement{}
for i := range a {
if _, ok := a[i].(AssociationCreatableStatement); ok {
stm = append(stm, a[i].(AssociationCreatableStatement))
}
}
return stm
}

// associationParams a wrapper for associations definition
// and creation.
Expand All @@ -65,22 +135,17 @@ type associationParams struct {
// see the builder defined in ./has_many_association.go as a guide of how to use it.
type associationBuilder func(associationParams) (Association, error)

// nullable means this type is a nullable association field.
type nullable interface {
Interface() interface{}
}

// fieldIsNil validates if a field has a nil reference. Also
// it validates if a field implements nullable interface and
// it has a nil value.
func fieldIsNil(f reflect.Value) bool {
null := (*nullable)(nil)
t := reflect.TypeOf(f.Interface())
if t.Implements(reflect.TypeOf(null).Elem()) {
m := reflect.ValueOf(f.Interface()).MethodByName("Interface")
out := m.Call([]reflect.Value{})
idValue := out[0].Interface()
return idValue == nil
if n := nulls.New(f.Interface()); n != nil {
return n.Interface() == nil
}
return f.Interface() == nil
}

func isZero(i interface{}) bool {
v := reflect.ValueOf(i)
return v.Interface() == reflect.Zero(v.Type()).Interface()
}
45 changes: 38 additions & 7 deletions associations/belongs_to_association.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package associations
import (
"fmt"
"reflect"

"github.com/gobuffalo/pop/nulls"
)

// belongsToAssociation is the implementation for the belongs_to
Expand All @@ -11,7 +13,8 @@ type belongsToAssociation struct {
ownerModel reflect.Value
ownerType reflect.Type
ownerID reflect.Value
owner interface{}
ownedModel interface{}
*associationSkipable
*associationComposite
}

Expand All @@ -28,16 +31,20 @@ func belongsToAssociationBuilder(p associationParams) (Association, error) {
}

// Validates if ownerIDField is nil, this association will be skipped.
var skipped bool
f := p.modelValue.FieldByName(ownerIDField)
if fieldIsNil(f) {
return SkippedAssociation, nil
if fieldIsNil(f) || isZero(f.Interface()) {
skipped = true
}

return &belongsToAssociation{
ownerModel: fval,
ownerType: fval.Type(),
ownerID: f,
owner: p.model,
ownerModel: fval,
ownerType: fval.Type(),
ownerID: f,
ownedModel: p.model,
associationSkipable: &associationSkipable{
skipped: skipped,
},
associationComposite: &associationComposite{innerAssociations: p.innerAssociations},
}, nil
}
Expand All @@ -63,3 +70,27 @@ func (b *belongsToAssociation) Interface() interface{} {
func (b *belongsToAssociation) Constraint() (string, []interface{}) {
return "id = ?", []interface{}{b.ownerID.Interface()}
}

func (b *belongsToAssociation) BeforeInterface() interface{} {
if !b.skipped {
return nil
}

if b.ownerModel.Kind() == reflect.Ptr {
return b.ownerModel.Interface()
}
return b.ownerModel.Addr().Interface()
}

func (b *belongsToAssociation) BeforeSetup() error {
ownerID := reflect.Indirect(reflect.ValueOf(b.ownerModel.Interface())).FieldByName("ID").Interface()
if b.ownerID.CanSet() {
if n := nulls.New(b.ownerID.Interface()); n != nil {
b.ownerID.Set(reflect.ValueOf(n.Parse(ownerID)))
} else {
b.ownerID.Set(reflect.ValueOf(ownerID))
}
return nil
}
return fmt.Errorf("could not set '%s' to '%s'", ownerID, b.ownerID)
}
Loading

0 comments on commit 4f62567

Please sign in to comment.