Repository: upper/db
Pull request #581 (2020-07-07 12:49:09)

ORM-like interface


Patch:
diff --git a/adapter/mongo/collection.go b/adapter/mongo/collection.go
index 1466953..9f21da7 100644
--- a/adapter/mongo/collection.go
+++ b/adapter/mongo/collection.go
@@ -254,6 +254,10 @@ func (col *Collection) Session() db.Session {
 	return col.parent
 }
 
+func (col *Collection) Count() (uint64, error) {
+	return col.Find().Count()
+}
+
 func (col *Collection) InsertReturning(item interface{}) error {
 	return db.ErrUnsupported
 }
diff --git a/adapter/mongo/database.go b/adapter/mongo/database.go
index 4bb8a44..d46955e 100644
--- a/adapter/mongo/database.go
+++ b/adapter/mongo/database.go
@@ -183,6 +183,14 @@ func (s *Source) Collections() (cols []db.Collection, err error) {
 	return cols, nil
 }
 
+func (s *Source) Get(db.Model, interface{}) error {
+	return db.ErrNotImplemented
+}
+
+func (s *Source) Save(db.Model) error {
+	return db.ErrNotImplemented
+}
+
 // Collection returns a collection by name.
 func (s *Source) Collection(name string) db.Collection {
 	s.collectionsMu.Lock()
diff --git a/collection.go b/collection.go
index a10a88d..f4cd7d8 100644
--- a/collection.go
+++ b/collection.go
@@ -34,6 +34,8 @@ type Collection interface {
 	// Find defines a new result set.
 	Find(...interface{}) Result
 
+	Count() (uint64, error)
+
 	// Insert inserts a new item into the collection, the type of this item could
 	// be a map, a struct or pointer to either of them. If the call succeeds and
 	// if the collection has a primary key, Insert returns the ID of the newly
diff --git a/errors.go b/errors.go
index d4b685f..0225392 100644
--- a/errors.go
+++ b/errors.go
@@ -144,7 +144,7 @@ var (
 	ErrUnsupportedDestination   = ErrWrongParameter.Wrap(newError(`unsupported destination type`))
 	ErrUnsupportedType          = ErrWrongParameter.Wrap(newError(`type does not support marshaling`))
 	ErrUnsupportedValue         = ErrWrongParameter.Wrap(newError(`value does not support unmarshaling`))
-	ErrNilItem                  = ErrWrongParameter.Wrap(newError(`invalid item`))
+	ErrNilItem                  = ErrWrongParameter.Wrap(newError(`invalid item (nil)`))
 	ErrZeroItemID               = ErrWrongParameter.Wrap(newError(`item ID is not defined`))
 	ErrMissingPrimaryKeys       = ErrWrongParameter.Wrap(newError(`collection has no primary keys`))
 
diff --git a/internal/sqladapter/collection.go b/internal/sqladapter/collection.go
index d7da5fa..ad01db9 100644
--- a/internal/sqladapter/collection.go
+++ b/internal/sqladapter/collection.go
@@ -9,6 +9,10 @@ import (
 	"github.com/upper/db/v4/sqlbuilder"
 )
 
+type hasReflector interface {
+	Reflect(db.Model)
+}
+
 // CollectionAdapter defines methods to be implemented by SQL adapters.
 type CollectionAdapter interface {
 	// Insert prepares and executes an INSERT statament. When the item is
@@ -34,6 +38,8 @@ type Collection interface {
 	// Find defined a new result set.
 	Find(conds ...interface{}) db.Result
 
+	Count() (uint64, error)
+
 	// Truncate removes all elements on the collection and resets the
 	// collection's IDs.
 	Truncate() error
@@ -100,6 +106,10 @@ func (c *collection) Name() string {
 	return c.name
 }
 
+func (c *collection) Count() (uint64, error) {
+	return c.Find().Count()
+}
+
 func (c *collection) Insert(item interface{}) (*db.InsertResult, error) {
 	id, err := c.adapter.Insert(c, item)
 	if err != nil {
@@ -234,6 +244,10 @@ func (c *collection) InsertReturning(item interface{}) error {
 		goto cancel
 	}
 
+	if reflector, ok := item.(hasReflector); ok {
+		reflector.Reflect(item.(db.Model))
+	}
+
 	if !inTx {
 		// This is only executed if t.Session() was **not** a transaction and if
 		// sess was created with sess.NewTransaction().
@@ -326,6 +340,10 @@ func (c *collection) UpdateReturning(item interface{}) error {
 		panic("default")
 	}
 
+	if reflector, ok := item.(hasReflector); ok {
+		reflector.Reflect(item.(db.Model))
+	}
+
 	if !inTx {
 		// This is only executed if t.Session() was **not** a transaction and if
 		// sess was created with sess.NewTransaction().
diff --git a/internal/sqladapter/database.go b/internal/sqladapter/database.go
index 2a9dedb..2ba17eb 100644
--- a/internal/sqladapter/database.go
+++ b/internal/sqladapter/database.go
@@ -110,8 +110,8 @@ type Session interface {
 	// Driver returns the underlying driver the session is using
 	Driver() interface{}
 
-	// Initializes an item.
-	Item(db.Model) db.Item
+	Save(db.Model) error
+	Get(db.Model, interface{}) error
 
 	// WaitForConnection attempts to run the given connection function a fixed
 	// number of times before failing.
@@ -316,8 +316,14 @@ func (sess *session) Open() error {
 	return sess.BindDB(sqlDB)
 }
 
-func (sess *session) Item(m db.Model) db.Item {
-	return newItem(sess, m)
+func (sess *session) Get(m db.Model, id interface{}) error {
+	return m.Collection(sess).Find(id).One(m)
+}
+
+func (sess *session) Save(m db.Model) error {
+	item := sqlbuilder.NewItem(sess, m)
+	item.(hasReflector).Reflect(m)
+	return item.Save(sess)
 }
 
 func (sess *session) DB() *sql.DB {
diff --git a/internal/unsupported/item.go b/internal/unsupported/item.go
index f21dbd8..5ae5084 100644
--- a/internal/unsupported/item.go
+++ b/internal/unsupported/item.go
@@ -28,15 +28,15 @@ import (
 type Item struct {
 }
 
-func (*Item) Update(db.M) error {
+func (*Item) Update(db.Session, db.M) error {
 	return db.ErrUnsupported
 }
 
-func (*Item) Delete() error {
+func (*Item) Delete(db.Session) error {
 	return db.ErrUnsupported
 }
 
-func (*Item) Save() error {
+func (*Item) Save(db.Session) error {
 	return db.ErrUnsupported
 }
 
diff --git a/item.go b/item.go
index 326d8bb..0ce17be 100644
--- a/item.go
+++ b/item.go
@@ -2,8 +2,8 @@ package db
 
 // Item provides methods for
 type Item interface {
-	Save() error
-	Delete() error
-	Update(M) error
+	Save(Session) error
+	Delete(Session) error
+	Update(Session, M) error
 	Changes() M
 }
diff --git a/session.go b/session.go
index 69d3d2a..09d1e6d 100644
--- a/session.go
+++ b/session.go
@@ -40,8 +40,8 @@ type Session interface {
 	// database.
 	Collections() ([]Collection, error)
 
-	// Item returns methods that operate on a single item of a model
-	Item(Model) Item
+	Save(model Model) error
+	Get(model Model, id interface{}) error
 
 	// Reset resets all the caching mechanisms the adapter is using.
 	Reset()
diff --git a/sqlbuilder/fetch.go b/sqlbuilder/fetch.go
index f6effa4..60e1669 100644
--- a/sqlbuilder/fetch.go
+++ b/sqlbuilder/fetch.go
@@ -32,6 +32,10 @@ type hasConvertValues interface {
 	ConvertValues(values []interface{}) []interface{}
 }
 
+type hasReflect interface {
+	Reflect(db.Model)
+}
+
 var Mapper = reflectx.NewMapper("db")
 
 // fetchRow receives a *sql.Rows value and tries to map all the rows into a
@@ -67,7 +71,6 @@ func fetchRow(iter *iterator, dst interface{}) error {
 
 	itemT := itemV.Type()
 	item, err := fetchResult(iter, itemT, columns)
-
 	if err != nil {
 		return err
 	}
@@ -78,6 +81,10 @@ func fetchRow(iter *iterator, dst interface{}) error {
 		itemV.Set(reflect.Indirect(item))
 	}
 
+	if reflector, ok := dst.(hasReflect); ok {
+		reflector.Reflect(dst.(db.Model))
+	}
+
 	return nil
 }
 
@@ -186,6 +193,7 @@ func fetchResult(iter *iterator, itemT reflect.Type, columns []string) (reflect.
 		if err = rows.Scan(values...); err != nil {
 			return item, err
 		}
+
 	case reflect.Map:
 
 		columns, err := rows.Columns()
diff --git a/internal/sqladapter/item.go b/sqlbuilder/item.go
similarity index 57%
rename from internal/sqladapter/item.go
rename to sqlbuilder/item.go
index a3193d5..18c16ef 100644
--- a/internal/sqladapter/item.go
+++ b/sqlbuilder/item.go
@@ -1,39 +1,48 @@
-package sqladapter
+package sqlbuilder
 
 import (
 	"reflect"
 
 	db "github.com/upper/db/v4"
-	"github.com/upper/db/v4/sqlbuilder"
 )
 
-type item struct {
-	sess     *session
+type hasPrimaryKeys interface {
+	PrimaryKeys() []string
+}
+
+type Item struct {
 	model    db.Model
 	pristine db.M
 }
 
+func (z *Item) Reflect(model db.Model) {
+	z.SetPristine(model)
+}
+
 func mapValues(model db.Model) db.M {
 	m := db.M{}
 	if model == nil {
 		return m
 	}
-	fieldMap := sqlbuilder.Mapper.FieldMap(reflect.ValueOf(model))
+	fieldMap := Mapper.FieldMap(reflect.ValueOf(model))
 	for column := range fieldMap {
 		m[column] = fieldMap[column].Interface()
 	}
 	return m
 }
 
-func newItem(sess *session, model db.Model) db.Item {
-	return &item{
-		sess:     sess,
-		model:    model,
-		pristine: mapValues(model),
-	}
+func NewItem(sess Session, model db.Model) db.Item {
+	item := &Item{}
+	item.SetPristine(model)
+	return item
+}
+
+func (z *Item) SetPristine(model db.Model) {
+	z.model = model               // current value
+	z.pristine = mapValues(model) // copy
 }
 
-func (z *item) Changes() db.M {
+func (z *Item) Changes() db.M {
 	diff := db.M{}
 	state := mapValues(z.model)
 	for column := range z.pristine {
@@ -45,9 +54,9 @@ func (z *item) Changes() db.M {
 	return diff
 }
 
-func (z *item) getPrimaryKeyFieldValues() ([]string, []interface{}) {
-	pKeys := z.model.Collection(z.sess).(Collection).PrimaryKeys()
-	fields := sqlbuilder.Mapper.FieldsByName(reflect.ValueOf(z.model), pKeys)
+func (z *Item) getPrimaryKeyFieldValues(sess db.Session) ([]string, []interface{}) {
+	pKeys := z.model.Collection(sess).(hasPrimaryKeys).PrimaryKeys()
+	fields := Mapper.FieldsByName(reflect.ValueOf(z.model), pKeys)
 
 	values := make([]interface{}, 0, len(fields))
 	for i := range fields {
@@ -59,14 +68,14 @@ func (z *item) getPrimaryKeyFieldValues() ([]string, []interface{}) {
 	return pKeys, values
 }
 
-func (z *item) id() (db.Cond, error) {
+func (z *Item) id(sess db.Session) (db.Cond, error) {
 	if z.model == nil {
 		return nil, db.ErrNilItem
 	}
 
 	id := db.Cond{}
 
-	keys, fields := z.getPrimaryKeyFieldValues()
+	keys, fields := z.getPrimaryKeyFieldValues(sess)
 	for i := range fields {
 		if fields[i] == reflect.Zero(reflect.TypeOf(fields[i])).Interface() {
 			return nil, db.ErrZeroItemID
@@ -80,7 +89,7 @@ func (z *item) id() (db.Cond, error) {
 	return id, nil
 }
 
-func (z *item) Save() error {
+func (z *Item) Save(sess db.Session) error {
 	if z.model == nil {
 		return db.ErrNilItem
 	}
@@ -89,7 +98,7 @@ func (z *item) Save() error {
 		return db.ErrExpectingPointerToStruct
 	}
 
-	_, fields := z.getPrimaryKeyFieldValues()
+	_, fields := z.getPrimaryKeyFieldValues(sess)
 	isCreate := true
 	for i := range fields {
 		if fields[i] != reflect.Zero(reflect.TypeOf(fields[i])).Interface() {
@@ -98,12 +107,12 @@ func (z *item) Save() error {
 	}
 
 	if isCreate {
-		return z.doCreate()
+		return z.doCreate(sess)
 	}
-	return z.doUpdate()
+	return z.doUpdate(sess)
 }
 
-func (z *item) doUpdate() error {
+func (z *Item) doUpdate(sess db.Session) error {
 	if validator, ok := z.model.(db.Validator); ok {
 		if err := validator.Validate(); err != nil {
 			return err
@@ -111,24 +120,24 @@ func (z *item) doUpdate() error {
 	}
 
 	if hook, ok := z.model.(db.BeforeUpdateHook); ok {
-		if err := hook.BeforeUpdate(z.sess); err != nil {
+		if err := hook.BeforeUpdate(sess); err != nil {
 			return err
 		}
 	}
 
-	if err := z.model.Collection(z.sess).UpdateReturning(z.model); err != nil {
+	if err := z.model.Collection(sess).UpdateReturning(z.model); err != nil {
 		return err
 	}
 
 	if hook, ok := z.model.(db.AfterUpdateHook); ok {
-		if err := hook.AfterUpdate(z.sess); err != nil {
+		if err := hook.AfterUpdate(sess); err != nil {
 			return err
 		}
 	}
 	return nil
 }
 
-func (z *item) doCreate() error {
+func (z *Item) doCreate(sess db.Session) error {
 	if validator, ok := z.model.(db.Validator); ok {
 		if err := validator.Validate(); err != nil {
 			return err
@@ -136,24 +145,24 @@ func (z *item) doCreate() error {
 	}
 
 	if hook, ok := z.model.(db.BeforeCreateHook); ok {
-		if err := hook.BeforeCreate(z.sess); err != nil {
+		if err := hook.BeforeCreate(sess); err != nil {
 			return err
 		}
 	}
 
-	if err := z.model.Collection(z.sess).InsertReturning(z.model); err != nil {
+	if err := z.model.Collection(sess).InsertReturning(z.model); err != nil {
 		return err
 	}
 
 	if hook, ok := z.model.(db.AfterCreateHook); ok {
-		if err := hook.AfterCreate(z.sess); err != nil {
+		if err := hook.AfterCreate(sess); err != nil {
 			return err
 		}
 	}
 	return nil
 }
 
-func (z *item) Delete() error {
+func (z *Item) Delete(sess db.Session) error {
 	if z.model == nil {
 		return db.ErrNilItem
 	}
@@ -162,23 +171,23 @@ func (z *item) Delete() error {
 		return db.ErrExpectingPointerToStruct
 	}
 
-	conds, err := z.id()
+	conds, err := z.id(sess)
 	if err != nil {
 		return err
 	}
 
 	if hook, ok := z.model.(db.BeforeDeleteHook); ok {
-		if err := hook.BeforeDelete(z.sess); err != nil {
+		if err := hook.BeforeDelete(sess); err != nil {
 			return err
 		}
 	}
 
-	if err := z.model.Collection(z.sess).Find(conds).Delete(); err != nil {
+	if err := z.model.Collection(sess).Find(conds).Delete(); err != nil {
 		return err
 	}
 
 	if hook, ok := z.model.(db.AfterDeleteHook); ok {
-		if err := hook.AfterDelete(z.sess); err != nil {
+		if err := hook.AfterDelete(sess); err != nil {
 			return err
 		}
 	}
@@ -186,7 +195,7 @@ func (z *item) Delete() error {
 	return nil
 }
 
-func (z *item) Update(m db.M) error {
+func (z *Item) Update(sess db.Session, m db.M) error {
 	if z.model == nil {
 		return db.ErrNilItem
 	}
@@ -195,23 +204,23 @@ func (z *item) Update(m db.M) error {
 		return db.ErrExpectingPointerToStruct
 	}
 
-	conds, err := z.id()
+	conds, err := z.id(sess)
 	if err != nil {
 		return err
 	}
 
 	if hook, ok := z.model.(db.BeforeDeleteHook); ok {
-		if err := hook.BeforeDelete(z.sess); err != nil {
+		if err := hook.BeforeDelete(sess); err != nil {
 			return err
 		}
 	}
 
-	if err := z.model.Collection(z.sess).Find(conds).Update(m); err != nil {
+	if err := z.model.Collection(sess).Find(conds).Update(m); err != nil {
 		return err
 	}
 
 	if hook, ok := z.model.(db.AfterDeleteHook); ok {
-		if err := hook.AfterDelete(z.sess); err != nil {
+		if err := hook.AfterDelete(sess); err != nil {
 			return err
 		}
 	}


Test patch:
diff --git a/internal/testsuite/bond_suite.go b/internal/testsuite/bond_suite.go
index edc377a..440d91e 100644
--- a/internal/testsuite/bond_suite.go
+++ b/internal/testsuite/bond_suite.go
@@ -2,7 +2,6 @@ package testsuite
 
 import (
 	"fmt"
-	"log"
 	"time"
 
 	"database/sql"
@@ -16,8 +15,8 @@ type Session struct {
 	sqlbuilder.Session
 }
 
-func (d *Session) AccountCollection() *AccountCollection {
-	return NewAccountCollection(d.Session)
+func (d *Session) Accounts() *Accounts {
+	return NewAccounts(d.Session)
 }
 
 func (d *Session) UserCollection() *UserCollection {
@@ -32,8 +31,8 @@ func NewSession(sess db.Session) *Session {
 	return &Session{Session: sess.(sqlbuilder.Session)}
 }
 
-func NewAccountCollection(sess db.Session) *AccountCollection {
-	return &AccountCollection{Collection: sess.Collection("accounts")}
+func NewAccounts(sess db.Session) *Accounts {
+	return &Accounts{Collection: sess.Collection("accounts")}
 }
 
 func NewUserCollection(sess db.Session) *UserCollection {
@@ -45,45 +44,44 @@ func NewLogCollection(sess db.Session) *LogCollection {
 }
 
 type Log struct {
+	sqlbuilder.Item
+
 	ID      uint64 `db:"id,omitempty"`
 	Message string `db:"message"`
 }
 
 type Account struct {
+	sqlbuilder.Item
+
 	ID        uint64     `db:"id,omitempty"`
 	Name      string     `db:"name"`
 	Disabled  bool       `db:"disabled"`
 	CreatedAt *time.Time `db:"created_at,omitempty"`
 }
 
-func (a *Account) Collection(sess db.Session) db.Collection {
+func (account *Account) Collection(sess db.Session) db.Collection {
 	return sess.Collection("accounts")
 }
 
-func (a *Account) AfterCreate(sess db.Session) error {
-	message := fmt.Sprintf("Account %q was created.", a.Name)
-	return sess.Item(&Log{Message: message}).Save()
-}
-
-func (a *Account) BeforeDelete() error {
-	// TODO: we should have flags on the object that we set here..
-	// and easily reset.. for testing
-	log.Println("beforedelete()..")
-	return nil
+func (account *Account) AfterCreate(sess db.Session) error {
+	message := fmt.Sprintf("Account %q was created.", account.Name)
+	return sess.Save(&Log{Message: message})
 }
 
 type User struct {
+	sqlbuilder.Item
+
 	ID        uint64 `db:"id,omitempty"`
 	AccountID uint64 `db:"account_id"`
 	Username  string `db:"username"`
 }
 
-func (u *User) AfterCreate(sess db.Session) error {
-	message := fmt.Sprintf("User %q was created.", u.Username)
-	return sess.Item(&Log{Message: message}).Save()
+func (user *User) AfterCreate(sess db.Session) error {
+	message := fmt.Sprintf("User %q was created.", user.Username)
+	return sess.Save(&Log{Message: message})
 }
 
-func (u *User) Collection(sess db.Session) db.Collection {
+func (user *User) Collection(sess db.Session) db.Collection {
 	return sess.Collection("users")
 }
 
@@ -95,16 +93,10 @@ type LogCollection struct {
 	db.Collection
 }
 
-type AccountCollection struct {
+type Accounts struct {
 	db.Collection
 }
 
-func (s AccountCollection) FindOne(cond db.Cond) (*Account, error) {
-	var a *Account
-	err := s.Find(cond).One(&a)
-	return a, err
-}
-
 type UserCollection struct {
 	db.Collection
 }
@@ -141,73 +133,109 @@ func (s *BondTestSuite) Session() *Session {
 	return s.sess
 }
 
-func (s *BondTestSuite) TestAccountCollection() {
+func (s *BondTestSuite) TestFindOne() {
+	var err error
 	sess := s.Session()
 
-	user := User{Username: "peter"}
+	user := User{Username: "jose"}
+	err = sess.Save(&user)
+	s.NoError(err)
+
+	s.NotZero(user.ID)
+
+	s.Equal(db.M{}, user.Changes())
+	user.Username = "jose-2"
+	s.Equal(db.M{"username": "jose-2"}, user.Changes())
+
+	user = User{}
+	err = sess.Get(&user, db.Cond{"username": "jose"})
 
-	err := sess.Item(&user).Save()
 	s.NoError(err)
 
-	err = sess.Item(&User{Username: "peter"}).Save()
-	s.Error(err, "Should fail because user is a unique value")
+	s.Equal(db.M{}, user.Changes())
+	user.Username = "catalina"
+	s.Equal(db.M{"username": "catalina"}, user.Changes())
 
-	account1 := Account{Name: "Pressly"}
-	err = sess.Item(&account1).Save()
+	err = sess.Save(&user)
+	s.NoError(err)
+
+	s.Equal(db.M{}, user.Changes())
+	user.Username = "nala"
+	s.Equal(db.M{"username": "nala"}, user.Changes())
+
+	err = sess.Save(&user)
 	s.NoError(err)
+	s.Equal(db.M{}, user.Changes())
+
+	userID := user.ID
 
-	account2 := &Account{}
+	user = User{}
+	err = sess.Get(&user, userID)
 
-	err = sess.AccountCollection().
-		Find(account1.ID).
-		One(&account2)
+	s.Equal("nala", user.Username)
 
+	user = User{}
+	err = sess.Get(&user, userID)
 	s.NoError(err)
-	s.Equal(account1.Name, account2.Name)
+	s.Equal("nala", user.Username)
 
-	var account3 Account
-	err = sess.Collection("accounts").
-		Find(account1.ID).
-		One(&account3)
+	err = user.Delete(sess)
+	s.NoError(err)
+
+	err = sess.Get(&user, userID)
+	s.Error(err)
 
+	err = sess.Collection("users").
+		Find(userID).
+		One(&user)
+	s.Error(err)
+}
+
+func (s *BondTestSuite) TestAccounts() {
+	sess := s.Session()
+
+	user := User{Username: "peter"}
+
+	err := sess.Save(&user)
 	s.NoError(err)
-	s.Equal(account1.Name, account3.Name)
 
-	colName := sess.Collection("accounts").Name()
-	s.Equal("accounts", colName)
+	user = User{Username: "peter"}
+	err = sess.Save(&user)
+	s.Error(err, "username should be unique")
 
-	count, err := sess.AccountCollection().
-		Find(db.Cond{}).
-		Count()
+	account1 := Account{Name: "Pressly"}
+	err = sess.Save(&account1)
 	s.NoError(err)
-	s.True(count == 1)
 
-	count, err = sess.AccountCollection().
-		Find().
-		Count()
+	account2 := Account{}
+	err = sess.Get(&account2, account1.ID)
+
 	s.NoError(err)
-	s.True(count == 1)
+	s.Equal(account1.Name, account2.Name)
+
+	var account3 Account
+	err = sess.Get(&account3, account1.ID)
+
+	s.NoError(err)
+	s.Equal(account1.Name, account3.Name)
 
 	var a Account
-	err = sess.AccountCollection().
-		Find(db.Cond{"id": account1.ID}).One(&a)
+	err = sess.Get(&a, account1.ID)
 	s.NoError(err)
 	s.NotNil(a)
 
 	account1.Disabled = true
-	err = sess.Item(&account1).Save()
+	err = sess.Save(&account1)
 	s.NoError(err)
 
-	count, err = sess.AccountCollection().
-		Find(db.Cond{}).
-		Count()
+	count, err := sess.Accounts().Count()
 	s.NoError(err)
 	s.Equal(uint64(1), count)
 
-	err = sess.Item(&account1).Delete()
+	err = account1.Delete(sess)
 	s.NoError(err)
 
-	count, err = sess.AccountCollection().Find().Count()
+	count, err = sess.Accounts().Find().Count()
 	s.NoError(err)
 	s.Zero(count)
 }
@@ -216,38 +244,31 @@ func (s *BondTestSuite) TestDelete() {
 	sess := s.Session()
 
 	account := Account{Name: "Pressly"}
-	err := sess.Item(&account).
-		Save()
+	err := sess.Save(&account)
 	s.NoError(err)
 	s.NotZero(account.ID)
 
 	// Delete by query -- without callbacks
-	err = sess.AccountCollection().
+	err = sess.Accounts().
 		Find(account.ID).
 		Delete()
 	s.NoError(err)
 
-	count, err := sess.AccountCollection().
-		Find(account.ID).
-		Count()
+	count, err := sess.Accounts().Find(account.ID).Count()
 	s.Zero(count)
 }
 
 func (s *BondTestSuite) TestSlices() {
 	sess := s.Session()
 
-	id, err := sess.AccountCollection().
-		Insert(&Account{Name: "Apple"})
+	err := sess.Save(&Account{Name: "Apple"})
 	s.NoError(err)
-	s.NotZero(id)
 
-	id, err = sess.AccountCollection().
-		Insert(Account{Name: "Google"})
+	err = sess.Save(&Account{Name: "Google"})
 	s.NoError(err)
-	s.NotZero(id)
 
 	var accounts []*Account
-	err = sess.AccountCollection().
+	err = sess.Accounts().
 		Find(db.Cond{}).
 		All(&accounts)
 	s.NoError(err)
@@ -257,22 +278,18 @@ func (s *BondTestSuite) TestSlices() {
 func (s *BondTestSuite) TestSelectOnlyIDs() {
 	sess := s.Session()
 
-	id, err := sess.AccountCollection().
-		Insert(&Account{Name: "Apple"})
+	err := sess.Save(&Account{Name: "Apple"})
 	s.NoError(err)
-	s.NotZero(id)
 
-	id, err = sess.AccountCollection().
-		Insert(Account{Name: "Google"})
+	err = sess.Save(&Account{Name: "Google"})
 	s.NoError(err)
-	s.NotZero(id)
 
 	var ids []struct {
 		Id int64 `db:"id"`
 	}
 
-	err = sess.AccountCollection().
-		Find(db.Cond{}).
+	err = sess.Accounts().
+		Find().
 		Select("id").All(&ids)
 	s.NoError(err)
 	s.Len(ids, 2)
@@ -283,31 +300,31 @@ func (s *BondTestSuite) TestTx() {
 	sess := s.Session()
 
 	user := User{Username: "peter"}
-	err := sess.Item(&user).Save()
+	err := sess.Save(&user)
 	s.NoError(err)
 
 	// This transaction should fail because user is a UNIQUE value and we already
 	// have a "peter".
 	err = sess.Tx(func(tx sqlbuilder.Tx) error {
-		return tx.Item(&User{Username: "peter"}).Save()
+		return tx.Save(&User{Username: "peter"})
 	})
 	s.Error(err)
 
 	// This transaction should fail because user is a UNIQUE value and we already
 	// have a "peter".
 	err = sess.Tx(func(tx sqlbuilder.Tx) error {
-		return tx.Item(&User{Username: "peter"}).Save()
+		return tx.Save(&User{Username: "peter"})
 	})
 	s.Error(err)
 
 	// This transaction will have no errors, but we'll produce one in order for
 	// it to rollback at the last moment.
 	err = sess.Tx(func(tx sqlbuilder.Tx) error {
-		if err := tx.Item(&User{Username: "Joe"}).Save(); err != nil {
+		if err := tx.Save(&User{Username: "Joe"}); err != nil {
 			return err
 		}
 
-		if err := tx.Item(&User{Username: "Cool"}).Save(); err != nil {
+		if err := tx.Save(&User{Username: "Cool"}); err != nil {
 			return err
 		}
 
@@ -318,11 +335,11 @@ func (s *BondTestSuite) TestTx() {
 	// Attempt to add two new unique values, if the transaction above had not
 	// been rolled back this transaction will fail.
 	err = sess.Tx(func(tx sqlbuilder.Tx) error {
-		if err := tx.Item(&User{Username: "Joe"}).Save(); err != nil {
+		if err := tx.Save(&User{Username: "Joe"}); err != nil {
 			return err
 		}
 
-		if err := tx.Item(&User{Username: "Cool"}).Save(); err != nil {
+		if err := tx.Save(&User{Username: "Cool"}); err != nil {
 			return err
 		}
 
@@ -332,11 +349,11 @@ func (s *BondTestSuite) TestTx() {
 
 	// If the transaction above was successful, this one will fail.
 	err = sess.Tx(func(tx sqlbuilder.Tx) error {
-		if err := tx.Item(&User{Username: "Joe"}).Save(); err != nil {
+		if err := tx.Save(&User{Username: "Joe"}); err != nil {
 			return err
 		}
 
-		if err := tx.Item(&User{Username: "Cool"}).Save(); err != nil {
+		if err := tx.Save(&User{Username: "Cool"}); err != nil {
 			return err
 		}
 
@@ -351,7 +368,7 @@ func (s *BondTestSuite) TestInheritedTx() {
 	sqlDB := sess.Driver().(*sql.DB)
 
 	user := User{Username: "peter"}
-	err := sess.Item(&user).Save()
+	err := sess.Save(&user)
 	s.NoError(err)
 
 	// Create a transaction
@@ -363,7 +380,7 @@ func (s *BondTestSuite) TestInheritedTx() {
 	s.NoError(err)
 
 	// Should fail because user is a UNIQUE value and we already have a "peter".
-	err = bondTx.Item(&User{Username: "peter"}).Save()
+	err = bondTx.Save(&User{Username: "peter"})
 	s.Error(err)
 
 	// The transaction is controlled outside bond.
@@ -371,7 +388,7 @@ func (s *BondTestSuite) TestInheritedTx() {
 	s.NoError(err)
 
 	// The sqlTx is worthless now.
-	err = bondTx.Item(&User{Username: "peter-2"}).Save()
+	err = bondTx.Save(&User{Username: "peter-2"})
 	s.Error(err)
 
 	// But we can create a new one.
@@ -384,14 +401,14 @@ func (s *BondTestSuite) TestInheritedTx() {
 	s.NoError(err)
 
 	// Adding two new values.
-	err = bondTx.Item(&User{Username: "Joe-2"}).Save()
+	err = bondTx.Save(&User{Username: "Joe-2"})
 	s.NoError(err)
 
-	err = bondTx.Item(&User{Username: "Cool-2"}).Save()
+	err = bondTx.Save(&User{Username: "Cool-2"})
 	s.NoError(err)
 
 	// And a value that is going to be rolled back.
-	err = bondTx.Item(&Account{Name: "Rolled back"}).Save()
+	err = bondTx.Save(&Account{Name: "Rolled back"})
 	s.NoError(err)
 
 	// This session happens to be a transaction, let's rollback everything.
@@ -406,14 +423,14 @@ func (s *BondTestSuite) TestInheritedTx() {
 	s.NoError(err)
 
 	// Attempt to add two unique values.
-	err = tx.Item(&User{Username: "Joe-2"}).Save()
+	err = tx.Save(&User{Username: "Joe-2"})
 	s.NoError(err)
 
-	err = tx.Item(&User{Username: "Cool-2"}).Save()
+	err = tx.Save(&User{Username: "Cool-2"})
 	s.NoError(err)
 
 	// And a value that is going to be commited.
-	err = tx.Item(&Account{Name: "Commited!"}).Save()
+	err = tx.Save(&Account{Name: "Commited!"})
 	s.NoError(err)
 
 	// Yes, commit them.
@@ -426,7 +443,7 @@ func (s *BondTestSuite) TestUnknownCollection() {
 
 	sess := s.Session()
 
-	err = sess.Item(nil).Save()
+	err = sess.Save(nil)
 	s.Error(err)
 
 	_, err = sess.Collection("users").Insert(&User{Username: "Foo"})
diff --git a/internal/unsupported/interfaces_test.go b/internal/unsupported/interfaces_test.go
new file mode 100644
index 0000000..709213d
--- /dev/null
+++ b/internal/unsupported/interfaces_test.go
@@ -0,0 +1,7 @@
+package unsupported
+
+import (
+	"github.com/upper/db/v4"
+)
+
+var _ db.Item = &Item{}
diff --git a/sqlbuilder/interfaces_test.go b/sqlbuilder/interfaces_test.go
new file mode 100644
index 0000000..b06a131
--- /dev/null
+++ b/sqlbuilder/interfaces_test.go
@@ -0,0 +1,7 @@
+package sqlbuilder
+
+import (
+	"github.com/upper/db/v4"
+)
+
+var _ db.Item = &Item{}