diff --git a/README.md b/README.md index 3b50dab1..34e30b79 100644 --- a/README.md +++ b/README.md @@ -12,41 +12,51 @@ Yet Another ORM library for Go, aims for developer friendly * Transaction * Logger Support * Bind struct with tag +* Iteration Support via sql.Rows +* sql.Scanner * Every feature comes with tests * Convention Over Configuration * Developer Friendly ## Conventions +* Table name is the plural of struct name's snake case. + Disable pluralization with `db.SingularTable(true)`, or [specify your table name](#specify-table-name) +* Column name is the snake case of field's name. +* Use `Id int64` field as primary key. +* Use tag `sql` to change field's property, change the tag name with `db.SetTagIdentifier(new_name)`. +* Use `CreatedAt` to store record's created time if it exist. +* Use `UpdatedAt` to store record's updated time if it exist. +* Use `DeletedAt` to store record's deleted time if it exist. [Soft Delete](#soft-delete) + ```go -// TableName: `users`, gorm will pluralize struct's name as table name, you are possible to turn off this feature type User struct { - Id int64 // Id: Database Primary key + Id int64 Birthday time.Time Age int64 - Name string `sql:"size:255"` // set this field's length in database - CreatedAt time.Time // Time of record is created, will be insert automatically - UpdatedAt time.Time // Time of record is updated, will be updated automatically - DeletedAt time.Time // Time of record is deleted, refer `Soft Delete` for more + Name string `sql:"size:255"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time Emails []Email // Embedded structs BillingAddress Address // Embedded struct - BillingAddressId sql.NullInt64 // Embedded struct BillingAddress's foreign key - ShippingAddress Address // Embedded struct - ShippingAddressId int64 // Embedded struct ShippingAddress's foreign key + BillingAddressId sql.NullInt64 // BillingAddress's foreign key + ShippingAddress Address // Another Embedded struct with same type + ShippingAddressId int64 // ShippingAddress's foreign key IgnoreMe int64 `sql:"-"` // Ignore this field } -type Email struct { // TableName: `emails` +type Email struct { Id int64 - UserId int64 // Foreign key for above embedded structs - Email string `sql:"type:varchar(100);"` // Set this field's type in database + UserId int64 // Foreign key for User + Email string `sql:"type:varchar(100);"` // Set this field's type Subscribed bool } -type Address struct { // TableName: `addresses` +type Address struct { Id int64 - Address1 string `sql:"not null;unique"` // Set this field as not null and unique in database + Address1 string `sql:"not null;unique"` // Set this field as not nullable and unique in database Address2 string `sql:"type:varchar(100);unique"` Post sql.NullString `sql:not null` // FYI, "NOT NULL" will only works well with NullXXX Scanner, because golang will initalize a default value for most type... @@ -735,7 +745,6 @@ db.Where("email = ?", "x@example.org").Attrs(User{RegisteredIp: "111.111.111.111 ``` ## TODO -* Rows, Row * Cache Stmt for performance * Join, Having, Group, Includes * Scopes, Valiations diff --git a/do.go b/do.go index db2974f4..143846c3 100644 --- a/do.go +++ b/do.go @@ -341,6 +341,16 @@ func (s *Do) related(value interface{}, foreign_keys ...string) *Do { return s } +func (s *Do) row() *sql.Row { + s.prepareQuerySql() + return s.db.db.QueryRow(s.sql, s.sqlVars...) +} + +func (s *Do) rows() (*sql.Rows, error) { + s.prepareQuerySql() + return s.db.db.Query(s.sql, s.sqlVars...) +} + func (s *Do) query() *Do { defer s.trace(time.Now()) var ( @@ -400,12 +410,8 @@ func (s *Do) query() *Do { func (s *Do) count(value interface{}) *Do { defer s.trace(time.Now()) - s.search = s.search.clone().selects("count(*)") - s.prepareQuerySql() - if !s.db.hasError() { - s.err(s.db.db.QueryRow(s.sql, s.sqlVars...).Scan(value)) - } + s.err(s.row().Scan(value)) return s } @@ -419,18 +425,13 @@ func (s *Do) pluck(column string, value interface{}) *Do { return s } - s.prepareQuerySql() - - if !s.db.hasError() { - rows, err := s.db.db.Query(s.sql, s.sqlVars...) - - if s.err(err) == nil { - defer rows.Close() - for rows.Next() { - dest := reflect.New(dest_out.Type().Elem()).Interface() - s.err(rows.Scan(dest)) - dest_out.Set(reflect.Append(dest_out, reflect.ValueOf(dest).Elem())) - } + rows, err := s.rows() + if s.err(err) == nil { + defer rows.Close() + for rows.Next() { + dest := reflect.New(dest_out.Type().Elem()).Interface() + s.err(rows.Scan(dest)) + dest_out.Set(reflect.Append(dest_out, reflect.ValueOf(dest).Elem())) } } return s diff --git a/gorm_test.go b/gorm_test.go index 38ec2b54..16ca7460 100644 --- a/gorm_test.go +++ b/gorm_test.go @@ -1364,6 +1364,33 @@ func TestQueryChain(t *testing.T) { } } +func TestRow(t *testing.T) { + row := db.Table("users").Where("name = ?", "2").Select("age").Row() + var age int64 + row.Scan(&age) + if age != 20 { + t.Errorf("Scan with Row") + } +} + +func TestRows(t *testing.T) { + rows, err := db.Table("users").Where("name = ?", "3").Select("name, age").Rows() + if err != nil { + t.Errorf("Not error should happen, but got", err) + } + + count := 0 + for rows.Next() { + var name string + var age int64 + rows.Scan(&name, &age) + count++ + } + if count != 2 { + t.Errorf("Should found two records with name 3") + } +} + func BenchmarkGorm(b *testing.B) { b.N = 2000 for x := 0; x < b.N; x++ { diff --git a/main.go b/main.go index c3b4027b..0648f6de 100644 --- a/main.go +++ b/main.go @@ -93,6 +93,14 @@ func (s *DB) Find(out interface{}, where ...interface{}) *DB { return s.clone().do(out).where(where...).query().db } +func (s *DB) Row() *sql.Row { + return s.do(s.data).row() +} + +func (s *DB) Rows() (*sql.Rows, error) { + return s.do(s.data).rows() +} + func (s *DB) Attrs(attrs ...interface{}) *DB { return s.clone().search.attrs(attrs...).db }