From f574429f5ec8bcaa984ca5edeb242bb552eba167 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 10 Jan 2016 21:26:55 +0800 Subject: [PATCH 01/83] Return pointer when Open --- main.go | 4 ++-- main_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 9fe6cf4e..f6cd66ad 100644 --- a/main.go +++ b/main.go @@ -36,7 +36,7 @@ type DB struct { joinTableHandlers map[string]JoinTableHandler } -func Open(dialect string, args ...interface{}) (DB, error) { +func Open(dialect string, args ...interface{}) (*DB, error) { var db DB var err error @@ -79,7 +79,7 @@ func Open(dialect string, args ...interface{}) (DB, error) { } } - return db, err + return &db, err } func (s *DB) Close() error { diff --git a/main_test.go b/main_test.go index e6c703e4..d288b8ad 100644 --- a/main_test.go +++ b/main_test.go @@ -20,7 +20,7 @@ import ( ) var ( - DB gorm.DB + DB *gorm.DB t1, t2, t3, t4, t5 time.Time ) @@ -41,7 +41,7 @@ func init() { runMigration() } -func OpenTestConnection() (db gorm.DB, err error) { +func OpenTestConnection() (db *gorm.DB, err error) { switch os.Getenv("GORM_DIALECT") { case "mysql": // CREATE USER 'gorm'@'localhost' IDENTIFIED BY 'gorm'; From 5c57885d985301d9b490522bd22e0f499a95931b Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 10 Jan 2016 21:33:15 +0800 Subject: [PATCH 02/83] DeletedAt's type has to been *time.Time --- delete_test.go | 2 +- join_table_test.go | 2 +- scope_private.go | 11 +++++++---- structs_test.go | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/delete_test.go b/delete_test.go index e0c71660..d3de0a6d 100644 --- a/delete_test.go +++ b/delete_test.go @@ -45,7 +45,7 @@ func TestSoftDelete(t *testing.T) { type User struct { Id int64 Name string - DeletedAt time.Time + DeletedAt *time.Time } DB.AutoMigrate(&User{}) diff --git a/join_table_test.go b/join_table_test.go index 3353aee2..ce92e42f 100644 --- a/join_table_test.go +++ b/join_table_test.go @@ -18,7 +18,7 @@ type PersonAddress struct { gorm.JoinTableHandler PersonID int AddressID int - DeletedAt time.Time + DeletedAt *time.Time CreatedAt time.Time } diff --git a/scope_private.go b/scope_private.go index d893c095..f8a68229 100644 --- a/scope_private.go +++ b/scope_private.go @@ -159,16 +159,19 @@ func (scope *Scope) buildSelectQuery(clause map[string]interface{}) (str string) } func (scope *Scope) whereSql() (sql string) { - var primaryConditions, andConditions, orConditions []string + var ( + quotedTableName = scope.QuotedTableName() + primaryConditions, andConditions, orConditions []string + ) - if !scope.Search.Unscoped && scope.Fields()["deleted_at"] != nil { - sql := fmt.Sprintf("(%v.deleted_at IS NULL OR %v.deleted_at <= '0001-01-02')", scope.QuotedTableName(), scope.QuotedTableName()) + if !scope.Search.Unscoped && scope.HasColumn("deleted_at") { + sql := fmt.Sprintf("%v.deleted_at IS NULL", quotedTableName) primaryConditions = append(primaryConditions, sql) } if !scope.PrimaryKeyZero() { for _, field := range scope.PrimaryFields() { - sql := fmt.Sprintf("(%v = %v)", scope.Quote(field.DBName), scope.AddToVars(field.Field.Interface())) + sql := fmt.Sprintf("%v.%v = %v", quotedTableName, scope.Quote(field.DBName), scope.AddToVars(field.Field.Interface())) primaryConditions = append(primaryConditions, sql) } } diff --git a/structs_test.go b/structs_test.go index a3dfa8b1..ef04cd4b 100644 --- a/structs_test.go +++ b/structs_test.go @@ -42,7 +42,7 @@ type CreditCard struct { UserId sql.NullInt64 CreatedAt time.Time UpdatedAt time.Time - DeletedAt time.Time + DeletedAt *time.Time } type Email struct { @@ -60,7 +60,7 @@ type Address struct { Post string CreatedAt time.Time UpdatedAt time.Time - DeletedAt time.Time + DeletedAt *time.Time } type Language struct { From f70de0bdff7c9ddf2126fc1f37e4c9adfdca1631 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 10 Jan 2016 22:56:22 +0800 Subject: [PATCH 03/83] Generate more friendly names for table and columns --- utils.go | 42 +++++++++++++++++++++++++++++++++++------- utils_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 utils_test.go diff --git a/utils.go b/utils.go index b457f321..158d8a3b 100644 --- a/utils.go +++ b/utils.go @@ -41,21 +41,49 @@ func newSafeMap() *safeMap { var smap = newSafeMap() +type Case bool + +const ( + lower Case = false + upper Case = true +) + func ToDBName(name string) string { if v := smap.Get(name); v != "" { return v } - value := commonInitialismsReplacer.Replace(name) - buf := bytes.NewBufferString("") - for i, v := range value { - if i > 0 && v >= 'A' && v <= 'Z' { - buf.WriteRune('_') + var ( + value = commonInitialismsReplacer.Replace(name) + buf = bytes.NewBufferString("") + lastCase, currCase, nextCase Case + ) + + for i, v := range value[:len(value)-1] { + nextCase = value[i+1] >= 'A' && value[i+1] <= 'Z' + if i > 0 { + if currCase == upper { + if lastCase == upper && nextCase == upper { + buf.WriteRune(v) + } else { + buf.WriteRune('_') + buf.WriteRune(v) + } + } else { + buf.WriteRune(v) + } + } else { + currCase = upper + buf.WriteRune(v) } - buf.WriteRune(v) + lastCase = currCase + currCase = nextCase } - s := strings.ToLower(buf.String()) + buf.WriteByte(value[len(value)-1]) + + s := strings.Replace(strings.ToLower(buf.String()), "__", "_", -1) + smap.Set(name, s) return s } diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 00000000..81b8ce9d --- /dev/null +++ b/utils_test.go @@ -0,0 +1,28 @@ +package gorm_test + +import ( + "testing" + + "github.com/jinzhu/gorm" +) + +func TestToDBNameGenerateFriendlyName(t *testing.T) { + var maps = map[string]string{ + "ThisIsATest": "this_is_a_test", + "PFAndESI": "pf_and_esi", + "AbcAndJkl": "abc_and_jkl", + "EmployeeID": "employee_id", + "HTTPAndSMTP": "http_and_smtp", + "HTTPServerHandlerForURLID": "http_server_handler_for_url_id", + "UUID": "uuid", + "HTTPURL": "http_url", + "HTTP_URL": "http_url", + "ThisIsActuallyATestSoWeMayBeAbleToUseThisCodeInGormPackageAlsoIdCanBeUsedAtTheEndAsID": "this_is_actually_a_test_so_we_may_be_able_to_use_this_code_in_gorm_package_also_id_can_be_used_at_the_end_as_id", + } + + for key, value := range maps { + if gorm.ToDBName(key) != value { + t.Errorf("%v ToDBName should equal %v, but got %v", key, value, gorm.ToDBName(key)) + } + } +} From 675283f0fe5ed665c1ccb9c18fb689976f69afc6 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 12 Jan 2016 15:47:47 +0800 Subject: [PATCH 04/83] Testing UpdateDecodeVirtualAttributes --- update_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/update_test.go b/update_test.go index 75877488..d483705c 100644 --- a/update_test.go +++ b/update_test.go @@ -419,3 +419,18 @@ func TestUpdateColumnsSkipsAssociations(t *testing.T) { t.Errorf("Expected user's BillingAddress.Address1=%s to remain unchanged after UpdateColumns invocation, but BillingAddress.Address1=%s", address1, freshUser.BillingAddress.Address1) } } + +func TestUpdateDecodeVirtualAttributes(t *testing.T) { + var user = User{ + Name: "jinzhu", + IgnoreMe: 88, + } + + DB.Save(&user) + + DB.Model(&user).Updates(User{Name: "jinzhu2", IgnoreMe: 100}) + + if user.IgnoreMe != 100 { + t.Errorf("should decode virtual attributes to struct, so it could be used in callbacks") + } +} From d53f5cf6ddf00a6cf670c5133a6dc57a080a85a7 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Wed, 13 Jan 2016 14:58:30 +0800 Subject: [PATCH 05/83] Rename scope.Trace to trace --- callback_create.go | 2 +- callback_query.go | 2 +- query_test.go | 4 +-- scope.go | 65 +++------------------------------------------- scope_private.go | 63 ++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 69 insertions(+), 67 deletions(-) diff --git a/callback_create.go b/callback_create.go index d13a71be..07121544 100644 --- a/callback_create.go +++ b/callback_create.go @@ -19,7 +19,7 @@ func UpdateTimeStampWhenCreate(scope *Scope) { } func Create(scope *Scope) { - defer scope.Trace(NowFunc()) + defer scope.trace(NowFunc()) if !scope.HasError() { // set create sql diff --git a/callback_query.go b/callback_query.go index 5473f232..75175b0d 100644 --- a/callback_query.go +++ b/callback_query.go @@ -7,7 +7,7 @@ import ( ) func Query(scope *Scope) { - defer scope.Trace(NowFunc()) + defer scope.trace(NowFunc()) var ( isSlice bool diff --git a/query_test.go b/query_test.go index 274e8e9b..71ced650 100644 --- a/query_test.go +++ b/query_test.go @@ -66,8 +66,8 @@ func TestUIntPrimaryKey(t *testing.T) { func TestStringPrimaryKeyForNumericValueStartingWithZero(t *testing.T) { type AddressByZipCode struct { - ZipCode string `gorm:"primary_key"` - Address string + ZipCode string `gorm:"primary_key"` + Address string } DB.AutoMigrate(&AddressByZipCode{}) diff --git a/scope.go b/scope.go index a11d4ec4..ef0763ce 100644 --- a/scope.go +++ b/scope.go @@ -5,7 +5,6 @@ import ( "fmt" "regexp" "strings" - "time" "reflect" ) @@ -265,7 +264,7 @@ type dbTabler interface { TableName(*DB) string } -// TableName get table name +// TableName return table name func (scope *Scope) TableName() string { if scope.Search != nil && len(scope.Search.tableName) > 0 { return scope.Search.tableName @@ -282,6 +281,7 @@ func (scope *Scope) TableName() string { return scope.GetModelStruct().TableName(scope.db.Model(scope.Value)) } +// QuotedTableName return quoted table name func (scope *Scope) QuotedTableName() (name string) { if scope.Search != nil && len(scope.Search.tableName) > 0 { if strings.Index(scope.Search.tableName, " ") != -1 { @@ -299,6 +299,7 @@ func (scope *Scope) CombinedConditionSql() string { scope.havingSql() + scope.orderSql() + scope.limitSql() + scope.offsetSql() } +// FieldByName find gorm.Field with name and db name func (scope *Scope) FieldByName(name string) (field *Field, ok bool) { for _, field := range scope.Fields() { if field.Name == name || field.DBName == name { @@ -316,7 +317,7 @@ func (scope *Scope) Raw(sql string) *Scope { // Exec invoke sql func (scope *Scope) Exec() *Scope { - defer scope.Trace(NowFunc()) + defer scope.trace(NowFunc()) if !scope.HasError() { if result, err := scope.SqlDB().Exec(scope.Sql, scope.SqlVars...); scope.Err(err) == nil { @@ -355,13 +356,6 @@ func (scope *Scope) InstanceGet(name string) (interface{}, bool) { return scope.Get(name + scope.InstanceId()) } -// Trace print sql log -func (scope *Scope) Trace(t time.Time) { - if len(scope.Sql) > 0 { - scope.db.slog(scope.Sql, t, scope.SqlVars...) - } -} - // Begin start a transaction func (scope *Scope) Begin() *Scope { if db, ok := scope.SqlDB().(sqlDb); ok { @@ -410,54 +404,3 @@ func (scope *Scope) SelectAttrs() []string { func (scope *Scope) OmitAttrs() []string { return scope.Search.omits } - -func (scope *Scope) changeableDBColumn(column string) bool { - selectAttrs := scope.SelectAttrs() - omitAttrs := scope.OmitAttrs() - - if len(selectAttrs) > 0 { - for _, attr := range selectAttrs { - if column == ToDBName(attr) { - return true - } - } - return false - } - - for _, attr := range omitAttrs { - if column == ToDBName(attr) { - return false - } - } - return true -} - -func (scope *Scope) changeableField(field *Field) bool { - selectAttrs := scope.SelectAttrs() - omitAttrs := scope.OmitAttrs() - - if len(selectAttrs) > 0 { - for _, attr := range selectAttrs { - if field.Name == attr || field.DBName == attr { - return true - } - } - return false - } - - for _, attr := range omitAttrs { - if field.Name == attr || field.DBName == attr { - return false - } - } - - return !field.IsIgnored -} - -func (scope *Scope) shouldSaveAssociations() bool { - saveAssociations, ok := scope.Get("gorm:save_associations") - if ok && !saveAssociations.(bool) { - return false - } - return true && !scope.HasError() -} diff --git a/scope_private.go b/scope_private.go index 135e7f92..a4d53366 100644 --- a/scope_private.go +++ b/scope_private.go @@ -8,6 +8,7 @@ import ( "regexp" "strconv" "strings" + "time" ) func (scope *Scope) primaryCondition(value interface{}) string { @@ -367,14 +368,14 @@ func (scope *Scope) updatedAttrsWithValues(values map[string]interface{}, ignore } func (scope *Scope) row() *sql.Row { - defer scope.Trace(NowFunc()) + defer scope.trace(NowFunc()) scope.callCallbacks(scope.db.parent.callback.rowQueries) scope.prepareQuerySql() return scope.SqlDB().QueryRow(scope.Sql, scope.SqlVars...) } func (scope *Scope) rows() (*sql.Rows, error) { - defer scope.Trace(NowFunc()) + defer scope.trace(NowFunc()) scope.callCallbacks(scope.db.parent.callback.rowQueries) scope.prepareQuerySql() return scope.SqlDB().Query(scope.Sql, scope.SqlVars...) @@ -425,6 +426,64 @@ func (scope *Scope) typeName() string { return typ.Name() } +// trace print sql log +func (scope *Scope) trace(t time.Time) { + if len(scope.Sql) > 0 { + scope.db.slog(scope.Sql, t, scope.SqlVars...) + } +} + +func (scope *Scope) changeableDBColumn(column string) bool { + selectAttrs := scope.SelectAttrs() + omitAttrs := scope.OmitAttrs() + + if len(selectAttrs) > 0 { + for _, attr := range selectAttrs { + if column == ToDBName(attr) { + return true + } + } + return false + } + + for _, attr := range omitAttrs { + if column == ToDBName(attr) { + return false + } + } + return true +} + +func (scope *Scope) changeableField(field *Field) bool { + selectAttrs := scope.SelectAttrs() + omitAttrs := scope.OmitAttrs() + + if len(selectAttrs) > 0 { + for _, attr := range selectAttrs { + if field.Name == attr || field.DBName == attr { + return true + } + } + return false + } + + for _, attr := range omitAttrs { + if field.Name == attr || field.DBName == attr { + return false + } + } + + return !field.IsIgnored +} + +func (scope *Scope) shouldSaveAssociations() bool { + saveAssociations, ok := scope.Get("gorm:save_associations") + if ok && !saveAssociations.(bool) { + return false + } + return true && !scope.HasError() +} + func (scope *Scope) related(value interface{}, foreignKeys ...string) *Scope { toScope := scope.db.NewScope(value) fromFields := scope.Fields() From bfd421f99920003d609e3ea20470c7a872641c3f Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Wed, 13 Jan 2016 10:11:31 +0800 Subject: [PATCH 06/83] Add TestNestedManyToManyPreload3 --- preload.go | 18 +++++++----- preload_test.go | 73 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 7 deletions(-) diff --git a/preload.go b/preload.go index d12995f3..15998c40 100644 --- a/preload.go +++ b/preload.go @@ -197,18 +197,20 @@ func (scope *Scope) handleBelongsToPreload(field *Field, conditions []interface{ } func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface{}) { - relation := field.Relationship - joinTableHandler := relation.JoinTableHandler - destType := field.StructField.Struct.Type.Elem() - var isPtr bool + var ( + relation = field.Relationship + joinTableHandler = relation.JoinTableHandler + destType = field.StructField.Struct.Type.Elem() + linkHash = make(map[string][]reflect.Value) + sourceKeys = []string{} + isPtr bool + ) + if destType.Kind() == reflect.Ptr { isPtr = true destType = destType.Elem() } - var sourceKeys []string - var linkHash = make(map[string][]reflect.Value) - for _, key := range joinTableHandler.SourceForeignKeys() { sourceKeys = append(sourceKeys, key.DBName) } @@ -217,9 +219,11 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface preloadJoinDB := joinTableHandler.JoinWith(joinTableHandler, db, scope.Value) + // preload inline conditions if len(conditions) > 0 { preloadJoinDB = preloadJoinDB.Where(conditions[0], conditions[1:]...) } + rows, err := preloadJoinDB.Rows() if scope.Err(err) != nil { diff --git a/preload_test.go b/preload_test.go index 29ea39a7..9e0716bd 100644 --- a/preload_test.go +++ b/preload_test.go @@ -1011,6 +1011,79 @@ func TestNestedManyToManyPreload2(t *testing.T) { } } +func TestNestedManyToManyPreload3(t *testing.T) { + type ( + Level1 struct { + ID uint + Value string + } + Level2 struct { + ID uint + Value string + Level1s []*Level1 `gorm:"many2many:level1_level2;"` + } + Level3 struct { + ID uint + Value string + Level2ID sql.NullInt64 + Level2 *Level2 + } + ) + + DB.DropTableIfExists(&Level1{}) + DB.DropTableIfExists(&Level2{}) + DB.DropTableIfExists(&Level3{}) + DB.DropTableIfExists("level1_level2") + + if err := DB.AutoMigrate(&Level3{}, &Level2{}, &Level1{}).Error; err != nil { + t.Error(err) + } + + level1Zh := &Level1{Value: "zh"} + level1Ru := &Level1{Value: "ru"} + level1En := &Level1{Value: "en"} + + level21 := &Level2{ + Value: "Level2-1", + Level1s: []*Level1{level1Zh, level1Ru}, + } + + level22 := &Level2{ + Value: "Level2-2", + Level1s: []*Level1{level1Zh, level1En}, + } + + wants := []*Level3{ + { + Value: "Level3-1", + Level2: level21, + }, + { + Value: "Level3-2", + Level2: level22, + }, + { + Value: "Level3-3", + Level2: level21, + }, + } + + for _, want := range wants { + if err := DB.Save(&want).Error; err != nil { + t.Error(err) + } + } + + var gots []*Level3 + if err := DB.Preload("Level2.Level1s").Find(&gots).Error; err != nil { + t.Error(err) + } + + if !reflect.DeepEqual(gots, wants) { + t.Errorf("got %s; want %s", toJSONString(gots), toJSONString(wants)) + } +} + func TestNilPointerSlice(t *testing.T) { type ( Level3 struct { From d9229c5a7be34df0c3a01fb92f59c7c9d6b8c8fe Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Wed, 13 Jan 2016 16:53:04 +0800 Subject: [PATCH 07/83] Extract method Scan from rows --- callback_query.go | 41 +++++------------------------------------ errors.go | 1 - preload_test.go | 2 ++ scope.go | 32 ++++++++++++++++++++++++++++++++ update_test.go | 2 ++ 5 files changed, 41 insertions(+), 37 deletions(-) diff --git a/callback_query.go b/callback_query.go index 75175b0d..f837d069 100644 --- a/callback_query.go +++ b/callback_query.go @@ -10,10 +10,9 @@ func Query(scope *Scope) { defer scope.trace(NowFunc()) var ( - isSlice bool - isPtr bool - anyRecordFound bool - destType reflect.Type + isSlice bool + isPtr bool + destType reflect.Type ) if orderBy, ok := scope.Get("gorm:order_by_primary_key"); ok { @@ -56,43 +55,13 @@ func Query(scope *Scope) { for rows.Next() { scope.db.RowsAffected++ - anyRecordFound = true elem := dest if isSlice { elem = reflect.New(destType).Elem() } - var values = make([]interface{}, len(columns)) - fields := scope.New(elem.Addr().Interface()).Fields() - - for index, column := range columns { - if field, ok := fields[column]; ok { - if field.Field.Kind() == reflect.Ptr { - values[index] = field.Field.Addr().Interface() - } else { - reflectValue := reflect.New(reflect.PtrTo(field.Struct.Type)) - reflectValue.Elem().Set(field.Field.Addr()) - values[index] = reflectValue.Interface() - } - } else { - var value interface{} - values[index] = &value - } - } - - scope.Err(rows.Scan(values...)) - - for index, column := range columns { - value := values[index] - if field, ok := fields[column]; ok { - if field.Field.Kind() == reflect.Ptr { - field.Field.Set(reflect.ValueOf(value).Elem()) - } else if v := reflect.ValueOf(value).Elem().Elem(); v.IsValid() { - field.Field.Set(v) - } - } - } + scope.scan(rows, columns, fields) if isSlice { if isPtr { @@ -103,7 +72,7 @@ func Query(scope *Scope) { } } - if !anyRecordFound && !isSlice { + if scope.db.RowsAffected == 0 && !isSlice { scope.Err(RecordNotFound) } } diff --git a/errors.go b/errors.go index 9dfcd2e9..c59dd968 100644 --- a/errors.go +++ b/errors.go @@ -8,7 +8,6 @@ import ( var ( RecordNotFound = errors.New("record not found") InvalidSql = errors.New("invalid sql") - NoNewAttrs = errors.New("no new attributes") NoValidTransaction = errors.New("no valid transaction") CantStartTransaction = errors.New("can't start transaction") ) diff --git a/preload_test.go b/preload_test.go index 9e0716bd..d2279e03 100644 --- a/preload_test.go +++ b/preload_test.go @@ -1012,6 +1012,8 @@ func TestNestedManyToManyPreload2(t *testing.T) { } func TestNestedManyToManyPreload3(t *testing.T) { + t.Skip("not implemented") + type ( Level1 struct { ID uint diff --git a/scope.go b/scope.go index ef0763ce..f7364e3d 100644 --- a/scope.go +++ b/scope.go @@ -1,6 +1,7 @@ package gorm import ( + "database/sql" "errors" "fmt" "regexp" @@ -404,3 +405,34 @@ func (scope *Scope) SelectAttrs() []string { func (scope *Scope) OmitAttrs() []string { return scope.Search.omits } + +func (scope *Scope) scan(rows *sql.Rows, columns []string, fields map[string]*Field) { + var values = make([]interface{}, len(columns)) + var ignored interface{} + + for index, column := range columns { + if field, ok := fields[column]; ok { + if field.Field.Kind() == reflect.Ptr { + values[index] = field.Field.Addr().Interface() + } else { + reflectValue := reflect.New(reflect.PtrTo(field.Struct.Type)) + reflectValue.Elem().Set(field.Field.Addr()) + values[index] = reflectValue.Interface() + } + } else { + values[index] = &ignored + } + } + + scope.Err(rows.Scan(values...)) + + for index, column := range columns { + if field, ok := fields[column]; ok { + if field.Field.Kind() != reflect.Ptr { + if v := reflect.ValueOf(values[index]).Elem().Elem(); v.IsValid() { + field.Field.Set(v) + } + } + } + } +} diff --git a/update_test.go b/update_test.go index d483705c..c3801c37 100644 --- a/update_test.go +++ b/update_test.go @@ -421,6 +421,8 @@ func TestUpdateColumnsSkipsAssociations(t *testing.T) { } func TestUpdateDecodeVirtualAttributes(t *testing.T) { + t.Skip("not implemented") + var user = User{ Name: "jinzhu", IgnoreMe: 88, From 79c4fae34d6e0f5aac66e619e0c62ad26963346c Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Fri, 15 Jan 2016 10:08:22 +0800 Subject: [PATCH 08/83] Refactor handleManyToManyPreload --- preload.go | 58 ++++++++++++++++++------------------------------------ 1 file changed, 19 insertions(+), 39 deletions(-) diff --git a/preload.go b/preload.go index 15998c40..354ed7a8 100644 --- a/preload.go +++ b/preload.go @@ -203,6 +203,8 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface destType = field.StructField.Struct.Type.Elem() linkHash = make(map[string][]reflect.Value) sourceKeys = []string{} + foreignKeyValue interface{} + foreignKeyType = reflect.ValueOf(&foreignKeyValue).Type() isPtr bool ) @@ -233,51 +235,29 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface columns, _ := rows.Columns() for rows.Next() { - elem := reflect.New(destType).Elem() - var values = make([]interface{}, len(columns)) + var ( + elem = reflect.New(destType).Elem() + fields = scope.New(elem.Addr().Interface()).Fields() + ) - fields := scope.New(elem.Addr().Interface()).Fields() - - var foundFields = map[string]bool{} - for index, column := range columns { - if field, ok := fields[column]; ok && !foundFields[column] { - if field.Field.Kind() == reflect.Ptr { - values[index] = field.Field.Addr().Interface() - } else { - values[index] = reflect.New(reflect.PtrTo(field.Field.Type())).Interface() - } - foundFields[column] = true - } else { - var i interface{} - values[index] = &i - } + // register foreign keys in join tables + for _, sourceKey := range sourceKeys { + fields[sourceKey] = &Field{Field: reflect.New(foreignKeyType).Elem()} } - scope.Err(rows.Scan(values...)) + scope.scan(rows, columns, fields) - var sourceKey []interface{} - - var scannedFields = map[string]bool{} - for index, column := range columns { - value := values[index] - if field, ok := fields[column]; ok && !scannedFields[column] { - if field.Field.Kind() == reflect.Ptr { - field.Field.Set(reflect.ValueOf(value).Elem()) - } else if v := reflect.ValueOf(value).Elem().Elem(); v.IsValid() { - field.Field.Set(v) - } - scannedFields[column] = true - } else if strInSlice(column, sourceKeys) { - sourceKey = append(sourceKey, *(value.(*interface{}))) - } + // generate hashed forkey keys in join table + var foreignKeys = make([]interface{}, len(sourceKeys)) + for idx, sourceKey := range sourceKeys { + foreignKeys[idx] = fields[sourceKey].Field.Elem().Interface() } + hashedSourceKeys := toString(foreignKeys) - if len(sourceKey) != 0 { - if isPtr { - linkHash[toString(sourceKey)] = append(linkHash[toString(sourceKey)], elem.Addr()) - } else { - linkHash[toString(sourceKey)] = append(linkHash[toString(sourceKey)], elem) - } + if isPtr { + linkHash[hashedSourceKeys] = append(linkHash[hashedSourceKeys], elem.Addr()) + } else { + linkHash[hashedSourceKeys] = append(linkHash[hashedSourceKeys], elem) } } From 41620f3d6cb6e7bf60289250594b61fa1b120374 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Fri, 15 Jan 2016 11:04:49 +0800 Subject: [PATCH 09/83] Fix assign relations to duplicated pointer fields --- preload.go | 40 ++++++------ preload_test.go | 170 ++++++++++++++++++++++++------------------------ 2 files changed, 103 insertions(+), 107 deletions(-) diff --git a/preload.go b/preload.go index 354ed7a8..f415386d 100644 --- a/preload.go +++ b/preload.go @@ -217,9 +217,8 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface sourceKeys = append(sourceKeys, key.DBName) } - db := scope.NewDB().Table(scope.New(reflect.New(destType).Interface()).TableName()).Select("*") - - preloadJoinDB := joinTableHandler.JoinWith(joinTableHandler, db, scope.Value) + preloadJoinDB := scope.NewDB().Table(scope.New(reflect.New(destType).Interface()).TableName()).Select("*") + preloadJoinDB = joinTableHandler.JoinWith(joinTableHandler, preloadJoinDB, scope.Value) // preload inline conditions if len(conditions) > 0 { @@ -261,31 +260,30 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface } } - var foreignFieldNames []string + // assign find results + var ( + indirectScopeValue = scope.IndirectValue() + fieldsSourceMap = map[string]reflect.Value{} + foreignFieldNames = []string{} + ) + for _, dbName := range relation.ForeignFieldNames { if field, ok := scope.FieldByName(dbName); ok { foreignFieldNames = append(foreignFieldNames, field.Name) } } - if scope.IndirectValue().Kind() == reflect.Slice { - objects := scope.IndirectValue() - for j := 0; j < objects.Len(); j++ { - object := reflect.Indirect(objects.Index(j)) - source := getRealValue(object, foreignFieldNames) - field := object.FieldByName(field.Name) - for _, link := range linkHash[toString(source)] { - field.Set(reflect.Append(field, link)) - } - } - } else { - if object := scope.IndirectValue(); object.IsValid() { - source := getRealValue(object, foreignFieldNames) - field := object.FieldByName(field.Name) - for _, link := range linkHash[toString(source)] { - field.Set(reflect.Append(field, link)) - } + if indirectScopeValue.Kind() == reflect.Slice { + for j := 0; j < indirectScopeValue.Len(); j++ { + object := reflect.Indirect(indirectScopeValue.Index(j)) + fieldsSourceMap[toString(getRealValue(object, foreignFieldNames))] = object.FieldByName(field.Name) } + } else if indirectScopeValue.IsValid() { + fieldsSourceMap[toString(getRealValue(indirectScopeValue, foreignFieldNames))] = indirectScopeValue.FieldByName(field.Name) + } + + for source, link := range linkHash { + fieldsSourceMap[source].Set(reflect.Append(fieldsSourceMap[source], link...)) } } diff --git a/preload_test.go b/preload_test.go index d2279e03..4f65d1d8 100644 --- a/preload_test.go +++ b/preload_test.go @@ -702,90 +702,6 @@ func TestManyToManyPreloadWithMultiPrimaryKeys(t *testing.T) { } } -func TestManyToManyPreloadForPointer(t *testing.T) { - type ( - Level1 struct { - ID uint - Value string - } - Level2 struct { - ID uint - Value string - Level1s []*Level1 `gorm:"many2many:levels;"` - } - ) - - DB.DropTableIfExists(&Level2{}) - DB.DropTableIfExists(&Level1{}) - DB.DropTableIfExists("levels") - - if err := DB.AutoMigrate(&Level2{}, &Level1{}).Error; err != nil { - t.Error(err) - } - - want := Level2{Value: "Bob", Level1s: []*Level1{ - {Value: "ru"}, - {Value: "en"}, - }} - if err := DB.Save(&want).Error; err != nil { - t.Error(err) - } - - want2 := Level2{Value: "Tom", Level1s: []*Level1{ - {Value: "zh"}, - {Value: "de"}, - }} - if err := DB.Save(&want2).Error; err != nil { - t.Error(err) - } - - var got Level2 - if err := DB.Preload("Level1s").Find(&got, "value = ?", "Bob").Error; err != nil { - t.Error(err) - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("got %s; want %s", toJSONString(got), toJSONString(want)) - } - - var got2 Level2 - if err := DB.Preload("Level1s").Find(&got2, "value = ?", "Tom").Error; err != nil { - t.Error(err) - } - - if !reflect.DeepEqual(got2, want2) { - t.Errorf("got %s; want %s", toJSONString(got2), toJSONString(want2)) - } - - var got3 []Level2 - if err := DB.Preload("Level1s").Find(&got3, "value IN (?)", []string{"Bob", "Tom"}).Error; err != nil { - t.Error(err) - } - - if !reflect.DeepEqual(got3, []Level2{got, got2}) { - t.Errorf("got %s; want %s", toJSONString(got3), toJSONString([]Level2{got, got2})) - } - - var got4 []Level2 - if err := DB.Preload("Level1s", "value IN (?)", []string{"zh", "ru"}).Find(&got4, "value IN (?)", []string{"Bob", "Tom"}).Error; err != nil { - t.Error(err) - } - - var got5 Level2 - DB.Preload("Level1s").First(&got5, "value = ?", "bogus") - - var ruLevel1 Level1 - var zhLevel1 Level1 - DB.First(&ruLevel1, "value = ?", "ru") - DB.First(&zhLevel1, "value = ?", "zh") - - got.Level1s = []*Level1{&ruLevel1} - got2.Level1s = []*Level1{&zhLevel1} - if !reflect.DeepEqual(got4, []Level2{got, got2}) { - t.Errorf("got %s; want %s", toJSONString(got4), toJSONString([]Level2{got, got2})) - } -} - func TestManyToManyPreloadForNestedPointer(t *testing.T) { type ( Level1 struct { @@ -1012,8 +928,6 @@ func TestNestedManyToManyPreload2(t *testing.T) { } func TestNestedManyToManyPreload3(t *testing.T) { - t.Skip("not implemented") - type ( Level1 struct { ID uint @@ -1086,6 +1000,90 @@ func TestNestedManyToManyPreload3(t *testing.T) { } } +func TestManyToManyPreloadForPointer(t *testing.T) { + type ( + Level1 struct { + ID uint + Value string + } + Level2 struct { + ID uint + Value string + Level1s []*Level1 `gorm:"many2many:levels;"` + } + ) + + DB.DropTableIfExists(&Level2{}) + DB.DropTableIfExists(&Level1{}) + DB.DropTableIfExists("levels") + + if err := DB.AutoMigrate(&Level2{}, &Level1{}).Error; err != nil { + t.Error(err) + } + + want := Level2{Value: "Bob", Level1s: []*Level1{ + {Value: "ru"}, + {Value: "en"}, + }} + if err := DB.Save(&want).Error; err != nil { + t.Error(err) + } + + want2 := Level2{Value: "Tom", Level1s: []*Level1{ + {Value: "zh"}, + {Value: "de"}, + }} + if err := DB.Save(&want2).Error; err != nil { + t.Error(err) + } + + var got Level2 + if err := DB.Preload("Level1s").Find(&got, "value = ?", "Bob").Error; err != nil { + t.Error(err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("got %s; want %s", toJSONString(got), toJSONString(want)) + } + + var got2 Level2 + if err := DB.Preload("Level1s").Find(&got2, "value = ?", "Tom").Error; err != nil { + t.Error(err) + } + + if !reflect.DeepEqual(got2, want2) { + t.Errorf("got %s; want %s", toJSONString(got2), toJSONString(want2)) + } + + var got3 []Level2 + if err := DB.Preload("Level1s").Find(&got3, "value IN (?)", []string{"Bob", "Tom"}).Error; err != nil { + t.Error(err) + } + + if !reflect.DeepEqual(got3, []Level2{got, got2}) { + t.Errorf("got %s; want %s", toJSONString(got3), toJSONString([]Level2{got, got2})) + } + + var got4 []Level2 + if err := DB.Preload("Level1s", "value IN (?)", []string{"zh", "ru"}).Find(&got4, "value IN (?)", []string{"Bob", "Tom"}).Error; err != nil { + t.Error(err) + } + + var got5 Level2 + DB.Preload("Level1s").First(&got5, "value = ?", "bogus") + + var ruLevel1 Level1 + var zhLevel1 Level1 + DB.First(&ruLevel1, "value = ?", "ru") + DB.First(&zhLevel1, "value = ?", "zh") + + got.Level1s = []*Level1{&ruLevel1} + got2.Level1s = []*Level1{&zhLevel1} + if !reflect.DeepEqual(got4, []Level2{got, got2}) { + t.Errorf("got %s; want %s", toJSONString(got4), toJSONString([]Level2{got, got2})) + } +} + func TestNilPointerSlice(t *testing.T) { type ( Level3 struct { From 3326a4e69dd09e830f69155f2e45f295f9cba13d Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Fri, 15 Jan 2016 15:53:53 +0800 Subject: [PATCH 10/83] Refactor Preload --- preload.go | 224 ++++++++++++++++++++--------------------------- utils_private.go | 34 +++++++ 2 files changed, 127 insertions(+), 131 deletions(-) diff --git a/preload.go b/preload.go index f415386d..cfc65380 100644 --- a/preload.go +++ b/preload.go @@ -1,139 +1,109 @@ package gorm import ( - "database/sql/driver" "errors" "fmt" "reflect" "strings" ) -func getRealValue(value reflect.Value, columns []string) (results []interface{}) { - // If value is a nil pointer, Indirect returns a zero Value! - // Therefor we need to check for a zero value, - // as FieldByName could panic - if pointedValue := reflect.Indirect(value); pointedValue.IsValid() { - for _, column := range columns { - if pointedValue.FieldByName(column).IsValid() { - result := pointedValue.FieldByName(column).Interface() - if r, ok := result.(driver.Valuer); ok { - result, _ = r.Value() - } - results = append(results, result) - } - } - } - return -} - -func equalAsString(a interface{}, b interface{}) bool { - return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b) -} - +// Preload preload relations callback func Preload(scope *Scope) { if scope.Search.preload == nil || scope.HasError() { return } - preloadMap := map[string]bool{} - fields := scope.Fields() + var ( + preloadedMap = map[string]bool{} + fields = scope.Fields() + ) + for _, preload := range scope.Search.preload { - schema, conditions := preload.schema, preload.conditions - keys := strings.Split(schema, ".") - currentScope := scope - currentFields := fields - originalConditions := conditions - conditions = []interface{}{} - for i, key := range keys { - var found bool - if preloadMap[strings.Join(keys[:i+1], ".")] { - goto nextLoop - } + var ( + preloadFields = strings.Split(preload.schema, ".") + currentScope = scope + currentFields = fields + ) - if i == len(keys)-1 { - conditions = originalConditions - } + for idx, preloadField := range preloadFields { + var currentPreloadConditions []interface{} - for _, field := range currentFields { - if field.Name != key || field.Relationship == nil { - continue + // if not preloaded + if preloadKey := strings.Join(preloadFields[:idx+1], "."); !preloadedMap[preloadKey] { + + // assign search conditions to last preload + if idx == len(preloadFields)-1 { + currentPreloadConditions = preload.conditions } - found = true - switch field.Relationship.Kind { - case "has_one": - currentScope.handleHasOnePreload(field, conditions) - case "has_many": - currentScope.handleHasManyPreload(field, conditions) - case "belongs_to": - currentScope.handleBelongsToPreload(field, conditions) - case "many_to_many": - currentScope.handleManyToManyPreload(field, conditions) - default: - currentScope.Err(errors.New("not supported relation")) + for _, field := range currentFields { + if field.Name != preloadField || field.Relationship == nil { + continue + } + + switch field.Relationship.Kind { + case "has_one": + currentScope.handleHasOnePreload(field, currentPreloadConditions) + case "has_many": + currentScope.handleHasManyPreload(field, currentPreloadConditions) + case "belongs_to": + currentScope.handleBelongsToPreload(field, currentPreloadConditions) + case "many_to_many": + currentScope.handleManyToManyPreload(field, currentPreloadConditions) + default: + scope.Err(errors.New("unsupported relation")) + } + + preloadedMap[preloadKey] = true + break + } + + if !preloadedMap[preloadKey] { + scope.Err(fmt.Errorf("can't preload field %s for %s", preloadField, currentScope.GetModelStruct().ModelType)) + return } - break } - if !found { - value := reflect.ValueOf(currentScope.Value) - if value.Kind() == reflect.Slice && value.Type().Elem().Kind() == reflect.Interface { - value = value.Index(0).Elem() - } - scope.Err(fmt.Errorf("can't find field %s in %s", key, value.Type())) - return - } - - preloadMap[strings.Join(keys[:i+1], ".")] = true - - nextLoop: - if i < len(keys)-1 { - currentScope = currentScope.getColumnsAsScope(key) + // preload next level + if idx < len(preloadFields)-1 { + currentScope = currentScope.getColumnAsScope(preloadField) currentFields = currentScope.Fields() } } } - -} - -func makeSlice(typ reflect.Type) interface{} { - if typ.Kind() == reflect.Slice { - typ = typ.Elem() - } - sliceType := reflect.SliceOf(typ) - slice := reflect.New(sliceType) - slice.Elem().Set(reflect.MakeSlice(sliceType, 0, 0)) - return slice.Interface() } func (scope *Scope) handleHasOnePreload(field *Field, conditions []interface{}) { relation := field.Relationship + // get relations's primary keys primaryKeys := scope.getColumnAsArray(relation.AssociationForeignFieldNames) if len(primaryKeys) == 0 { return } + // find relations results := makeSlice(field.Struct.Type) scope.Err(scope.NewDB().Where(fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relation.ForeignDBNames), toQueryMarks(primaryKeys)), toQueryValues(primaryKeys)...).Find(results, conditions...).Error) - resultValues := reflect.Indirect(reflect.ValueOf(results)) - for i := 0; i < resultValues.Len(); i++ { - result := resultValues.Index(i) - if scope.IndirectValue().Kind() == reflect.Slice { - value := getRealValue(result, relation.ForeignFieldNames) - objects := scope.IndirectValue() - for j := 0; j < objects.Len(); j++ { - if equalAsString(getRealValue(objects.Index(j), relation.AssociationForeignFieldNames), value) { - reflect.Indirect(objects.Index(j)).FieldByName(field.Name).Set(result) + // assign find results + var ( + resultsValue = reflect.Indirect(reflect.ValueOf(results)) + indirectScopeValue = scope.IndirectValue() + ) + + for i := 0; i < resultsValue.Len(); i++ { + result := resultsValue.Index(i) + if indirectScopeValue.Kind() == reflect.Slice { + value := getValueFromFields(result, relation.ForeignFieldNames) + for j := 0; j < indirectScopeValue.Len(); j++ { + if equalAsString(getValueFromFields(indirectScopeValue.Index(j), relation.AssociationForeignFieldNames), value) { + reflect.Indirect(indirectScopeValue.Index(j)).FieldByName(field.Name).Set(result) break } } } else { - if err := scope.SetColumn(field, result); err != nil { - scope.Err(err) - return - } + scope.Err(field.Set(result)) } } } @@ -152,11 +122,11 @@ func (scope *Scope) handleHasManyPreload(field *Field, conditions []interface{}) if scope.IndirectValue().Kind() == reflect.Slice { for i := 0; i < resultValues.Len(); i++ { result := resultValues.Index(i) - value := getRealValue(result, relation.ForeignFieldNames) + value := getValueFromFields(result, relation.ForeignFieldNames) objects := scope.IndirectValue() for j := 0; j < objects.Len(); j++ { object := reflect.Indirect(objects.Index(j)) - if equalAsString(getRealValue(object, relation.AssociationForeignFieldNames), value) { + if equalAsString(getValueFromFields(object, relation.AssociationForeignFieldNames), value) { f := object.FieldByName(field.Name) f.Set(reflect.Append(f, result)) break @@ -182,11 +152,11 @@ func (scope *Scope) handleBelongsToPreload(field *Field, conditions []interface{ for i := 0; i < resultValues.Len(); i++ { result := resultValues.Index(i) if scope.IndirectValue().Kind() == reflect.Slice { - value := getRealValue(result, relation.AssociationForeignFieldNames) + value := getValueFromFields(result, relation.AssociationForeignFieldNames) objects := scope.IndirectValue() for j := 0; j < objects.Len(); j++ { object := reflect.Indirect(objects.Index(j)) - if equalAsString(getRealValue(object, relation.ForeignFieldNames), value) { + if equalAsString(getValueFromFields(object, relation.ForeignFieldNames), value) { object.FieldByName(field.Name).Set(result) } } @@ -276,10 +246,10 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface if indirectScopeValue.Kind() == reflect.Slice { for j := 0; j < indirectScopeValue.Len(); j++ { object := reflect.Indirect(indirectScopeValue.Index(j)) - fieldsSourceMap[toString(getRealValue(object, foreignFieldNames))] = object.FieldByName(field.Name) + fieldsSourceMap[toString(getValueFromFields(object, foreignFieldNames))] = object.FieldByName(field.Name) } } else if indirectScopeValue.IsValid() { - fieldsSourceMap[toString(getRealValue(indirectScopeValue, foreignFieldNames))] = indirectScopeValue.FieldByName(field.Name) + fieldsSourceMap[toString(getValueFromFields(indirectScopeValue, foreignFieldNames))] = indirectScopeValue.FieldByName(field.Name) } for source, link := range linkHash { @@ -308,46 +278,38 @@ func (scope *Scope) getColumnAsArray(columns []string) (results [][]interface{}) return } -func (scope *Scope) getColumnsAsScope(column string) *Scope { - values := scope.IndirectValue() - switch values.Kind() { +func (scope *Scope) getColumnAsScope(column string) *Scope { + indirectScopeValue := scope.IndirectValue() + + switch indirectScopeValue.Kind() { case reflect.Slice: - modelType := values.Type().Elem() - if modelType.Kind() == reflect.Ptr { - modelType = modelType.Elem() - } - fieldStruct, _ := modelType.FieldByName(column) - var columns reflect.Value - if fieldStruct.Type.Kind() == reflect.Slice || fieldStruct.Type.Kind() == reflect.Ptr { - columns = reflect.New(reflect.SliceOf(reflect.PtrTo(fieldStruct.Type.Elem()))).Elem() - } else { - columns = reflect.New(reflect.SliceOf(reflect.PtrTo(fieldStruct.Type))).Elem() - } - for i := 0; i < values.Len(); i++ { - column := reflect.Indirect(values.Index(i)).FieldByName(column) - if column.Kind() == reflect.Ptr { - column = column.Elem() + if fieldStruct, ok := scope.GetModelStruct().ModelType.FieldByName(column); ok { + fieldType := fieldStruct.Type + if fieldType.Kind() == reflect.Slice || fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() } - if column.Kind() == reflect.Slice { - for i := 0; i < column.Len(); i++ { - elem := column.Index(i) - if elem.CanAddr() { - columns = reflect.Append(columns, elem.Addr()) + + results := reflect.New(reflect.SliceOf(reflect.PtrTo(fieldType))).Elem() + + for i := 0; i < indirectScopeValue.Len(); i++ { + result := reflect.Indirect(reflect.Indirect(indirectScopeValue.Index(i)).FieldByName(column)) + + if result.Kind() == reflect.Slice { + for j := 0; j < result.Len(); j++ { + if elem := result.Index(j); elem.CanAddr() { + results = reflect.Append(results, elem.Addr()) + } } - } - } else { - if column.CanAddr() { - columns = reflect.Append(columns, column.Addr()) + } else if result.CanAddr() { + results = reflect.Append(results, result.Addr()) } } + return scope.New(results.Interface()) } - return scope.New(columns.Interface()) case reflect.Struct: - field := values.FieldByName(column) - if !field.CanAddr() { - return nil + if field := indirectScopeValue.FieldByName(column); field.CanAddr() { + return scope.New(field.Addr().Interface()) } - return scope.New(field.Addr().Interface()) } return nil } diff --git a/utils_private.go b/utils_private.go index 50549857..f8f918fb 100644 --- a/utils_private.go +++ b/utils_private.go @@ -1,6 +1,7 @@ package gorm import ( + "database/sql/driver" "fmt" "reflect" "regexp" @@ -73,6 +74,10 @@ func convertInterfaceToMap(values interface{}) map[string]interface{} { return attrs } +func equalAsString(a interface{}, b interface{}) bool { + return toString(a) == toString(b) +} + func toString(str interface{}) string { if values, ok := str.([]interface{}); ok { var results []string @@ -87,6 +92,16 @@ func toString(str interface{}) string { } } +func makeSlice(elemType reflect.Type) interface{} { + if elemType.Kind() == reflect.Slice { + elemType = elemType.Elem() + } + sliceType := reflect.SliceOf(elemType) + slice := reflect.New(sliceType) + slice.Elem().Set(reflect.MakeSlice(sliceType, 0, 0)) + return slice.Interface() +} + func strInSlice(a string, list []string) bool { for _, b := range list { if b == a { @@ -95,3 +110,22 @@ func strInSlice(a string, list []string) bool { } return false } + +// getValueFromFields return given fields's value +func getValueFromFields(value reflect.Value, fieldNames []string) (results []interface{}) { + // If value is a nil pointer, Indirect returns a zero Value! + // Therefor we need to check for a zero value, + // as FieldByName could panic + if indirectValue := reflect.Indirect(value); indirectValue.IsValid() { + for _, fieldName := range fieldNames { + if fieldValue := indirectValue.FieldByName(fieldName); fieldValue.IsValid() { + result := fieldValue.Interface() + if r, ok := result.(driver.Valuer); ok { + result, _ = r.Value() + } + results = append(results, result) + } + } + } + return +} From 551c1e0c20dfa10669db00f90afe7ff6c303eea9 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Fri, 15 Jan 2016 20:37:41 +0800 Subject: [PATCH 11/83] Keep refactoring on Preload --- preload.go | 135 +++++++++++++++++-------------------------------- scope_utils.go | 61 ++++++++++++++++++++++ 2 files changed, 108 insertions(+), 88 deletions(-) create mode 100644 scope_utils.go diff --git a/preload.go b/preload.go index cfc65380..2333cade 100644 --- a/preload.go +++ b/preload.go @@ -95,10 +95,10 @@ func (scope *Scope) handleHasOnePreload(field *Field, conditions []interface{}) for i := 0; i < resultsValue.Len(); i++ { result := resultsValue.Index(i) if indirectScopeValue.Kind() == reflect.Slice { - value := getValueFromFields(result, relation.ForeignFieldNames) + foreignValues := getValueFromFields(result, relation.ForeignFieldNames) for j := 0; j < indirectScopeValue.Len(); j++ { - if equalAsString(getValueFromFields(indirectScopeValue.Index(j), relation.AssociationForeignFieldNames), value) { - reflect.Indirect(indirectScopeValue.Index(j)).FieldByName(field.Name).Set(result) + if indirectValue := reflect.Indirect(indirectScopeValue.Index(j)); equalAsString(getValueFromFields(indirectValue, relation.AssociationForeignFieldNames), foreignValues) { + indirectValue.FieldByName(field.Name).Set(result) break } } @@ -110,58 +110,72 @@ func (scope *Scope) handleHasOnePreload(field *Field, conditions []interface{}) func (scope *Scope) handleHasManyPreload(field *Field, conditions []interface{}) { relation := field.Relationship + + // get relations's primary keys primaryKeys := scope.getColumnAsArray(relation.AssociationForeignFieldNames) if len(primaryKeys) == 0 { return } + // find relations results := makeSlice(field.Struct.Type) scope.Err(scope.NewDB().Where(fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relation.ForeignDBNames), toQueryMarks(primaryKeys)), toQueryValues(primaryKeys)...).Find(results, conditions...).Error) - resultValues := reflect.Indirect(reflect.ValueOf(results)) - if scope.IndirectValue().Kind() == reflect.Slice { - for i := 0; i < resultValues.Len(); i++ { - result := resultValues.Index(i) - value := getValueFromFields(result, relation.ForeignFieldNames) - objects := scope.IndirectValue() - for j := 0; j < objects.Len(); j++ { - object := reflect.Indirect(objects.Index(j)) - if equalAsString(getValueFromFields(object, relation.AssociationForeignFieldNames), value) { - f := object.FieldByName(field.Name) - f.Set(reflect.Append(f, result)) + // assign find results + var ( + resultsValue = reflect.Indirect(reflect.ValueOf(results)) + indirectScopeValue = scope.IndirectValue() + ) + + if indirectScopeValue.Kind() == reflect.Slice { + for i := 0; i < resultsValue.Len(); i++ { + result := resultsValue.Index(i) + foreignValues := getValueFromFields(result, relation.ForeignFieldNames) + for j := 0; j < indirectScopeValue.Len(); j++ { + object := reflect.Indirect(indirectScopeValue.Index(j)) + if equalAsString(getValueFromFields(object, relation.AssociationForeignFieldNames), foreignValues) { + objectField := object.FieldByName(field.Name) + objectField.Set(reflect.Append(objectField, result)) break } } } } else { - scope.SetColumn(field, resultValues) + scope.Err(field.Set(resultsValue)) } } func (scope *Scope) handleBelongsToPreload(field *Field, conditions []interface{}) { relation := field.Relationship + + // get relations's primary keys primaryKeys := scope.getColumnAsArray(relation.ForeignFieldNames) if len(primaryKeys) == 0 { return } + // find relations results := makeSlice(field.Struct.Type) scope.Err(scope.NewDB().Where(fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relation.AssociationForeignDBNames), toQueryMarks(primaryKeys)), toQueryValues(primaryKeys)...).Find(results, conditions...).Error) - resultValues := reflect.Indirect(reflect.ValueOf(results)) - for i := 0; i < resultValues.Len(); i++ { - result := resultValues.Index(i) - if scope.IndirectValue().Kind() == reflect.Slice { + // assign find results + var ( + resultsValue = reflect.Indirect(reflect.ValueOf(results)) + indirectScopeValue = scope.IndirectValue() + ) + + for i := 0; i < resultsValue.Len(); i++ { + result := resultsValue.Index(i) + if indirectScopeValue.Kind() == reflect.Slice { value := getValueFromFields(result, relation.AssociationForeignFieldNames) - objects := scope.IndirectValue() - for j := 0; j < objects.Len(); j++ { - object := reflect.Indirect(objects.Index(j)) + for j := 0; j < indirectScopeValue.Len(); j++ { + object := reflect.Indirect(indirectScopeValue.Index(j)) if equalAsString(getValueFromFields(object, relation.ForeignFieldNames), value) { object.FieldByName(field.Name).Set(result) } } } else { - scope.SetColumn(field, result) + scope.Err(field.Set(result)) } } } @@ -170,24 +184,25 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface var ( relation = field.Relationship joinTableHandler = relation.JoinTableHandler - destType = field.StructField.Struct.Type.Elem() - linkHash = make(map[string][]reflect.Value) - sourceKeys = []string{} + fieldType = field.StructField.Struct.Type.Elem() foreignKeyValue interface{} foreignKeyType = reflect.ValueOf(&foreignKeyValue).Type() + linkHash = map[string][]reflect.Value{} isPtr bool ) - if destType.Kind() == reflect.Ptr { + if fieldType.Kind() == reflect.Ptr { isPtr = true - destType = destType.Elem() + fieldType = fieldType.Elem() } + var sourceKeys = []string{} for _, key := range joinTableHandler.SourceForeignKeys() { sourceKeys = append(sourceKeys, key.DBName) } - preloadJoinDB := scope.NewDB().Table(scope.New(reflect.New(destType).Interface()).TableName()).Select("*") + // generate query with join table + preloadJoinDB := scope.NewDB().Table(scope.New(reflect.New(fieldType).Interface()).TableName()).Select("*") preloadJoinDB = joinTableHandler.JoinWith(joinTableHandler, preloadJoinDB, scope.Value) // preload inline conditions @@ -205,7 +220,7 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface columns, _ := rows.Columns() for rows.Next() { var ( - elem = reflect.New(destType).Elem() + elem = reflect.New(fieldType).Elem() fields = scope.New(elem.Addr().Interface()).Fields() ) @@ -235,10 +250,11 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface indirectScopeValue = scope.IndirectValue() fieldsSourceMap = map[string]reflect.Value{} foreignFieldNames = []string{} + fields = scope.Fields() ) for _, dbName := range relation.ForeignFieldNames { - if field, ok := scope.FieldByName(dbName); ok { + if field, ok := fields[dbName]; ok { foreignFieldNames = append(foreignFieldNames, field.Name) } } @@ -256,60 +272,3 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface fieldsSourceMap[source].Set(reflect.Append(fieldsSourceMap[source], link...)) } } - -func (scope *Scope) getColumnAsArray(columns []string) (results [][]interface{}) { - values := scope.IndirectValue() - switch values.Kind() { - case reflect.Slice: - for i := 0; i < values.Len(); i++ { - var result []interface{} - for _, column := range columns { - result = append(result, reflect.Indirect(values.Index(i)).FieldByName(column).Interface()) - } - results = append(results, result) - } - case reflect.Struct: - var result []interface{} - for _, column := range columns { - result = append(result, values.FieldByName(column).Interface()) - } - return [][]interface{}{result} - } - return -} - -func (scope *Scope) getColumnAsScope(column string) *Scope { - indirectScopeValue := scope.IndirectValue() - - switch indirectScopeValue.Kind() { - case reflect.Slice: - if fieldStruct, ok := scope.GetModelStruct().ModelType.FieldByName(column); ok { - fieldType := fieldStruct.Type - if fieldType.Kind() == reflect.Slice || fieldType.Kind() == reflect.Ptr { - fieldType = fieldType.Elem() - } - - results := reflect.New(reflect.SliceOf(reflect.PtrTo(fieldType))).Elem() - - for i := 0; i < indirectScopeValue.Len(); i++ { - result := reflect.Indirect(reflect.Indirect(indirectScopeValue.Index(i)).FieldByName(column)) - - if result.Kind() == reflect.Slice { - for j := 0; j < result.Len(); j++ { - if elem := result.Index(j); elem.CanAddr() { - results = reflect.Append(results, elem.Addr()) - } - } - } else if result.CanAddr() { - results = reflect.Append(results, result.Addr()) - } - } - return scope.New(results.Interface()) - } - case reflect.Struct: - if field := indirectScopeValue.FieldByName(column); field.CanAddr() { - return scope.New(field.Addr().Interface()) - } - } - return nil -} diff --git a/scope_utils.go b/scope_utils.go new file mode 100644 index 00000000..99957adc --- /dev/null +++ b/scope_utils.go @@ -0,0 +1,61 @@ +package gorm + +import "reflect" + +func (scope *Scope) getColumnAsArray(columns []string) (results [][]interface{}) { + indirectScopeValue := scope.IndirectValue() + switch indirectScopeValue.Kind() { + case reflect.Slice: + for i := 0; i < indirectScopeValue.Len(); i++ { + var result []interface{} + var object = reflect.Indirect(indirectScopeValue.Index(i)) + for _, column := range columns { + result = append(result, object.FieldByName(column).Interface()) + } + results = append(results, result) + } + case reflect.Struct: + var result []interface{} + for _, column := range columns { + result = append(result, indirectScopeValue.FieldByName(column).Interface()) + } + return [][]interface{}{result} + } + return +} + +func (scope *Scope) getColumnAsScope(column string) *Scope { + indirectScopeValue := scope.IndirectValue() + + switch indirectScopeValue.Kind() { + case reflect.Slice: + if fieldStruct, ok := scope.GetModelStruct().ModelType.FieldByName(column); ok { + fieldType := fieldStruct.Type + if fieldType.Kind() == reflect.Slice || fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() + } + + results := reflect.New(reflect.SliceOf(reflect.PtrTo(fieldType))).Elem() + + for i := 0; i < indirectScopeValue.Len(); i++ { + result := reflect.Indirect(reflect.Indirect(indirectScopeValue.Index(i)).FieldByName(column)) + + if result.Kind() == reflect.Slice { + for j := 0; j < result.Len(); j++ { + if elem := result.Index(j); elem.CanAddr() { + results = reflect.Append(results, elem.Addr()) + } + } + } else if result.CanAddr() { + results = reflect.Append(results, result.Addr()) + } + } + return scope.New(results.Interface()) + } + case reflect.Struct: + if field := indirectScopeValue.FieldByName(column); field.CanAddr() { + return scope.New(field.Addr().Interface()) + } + } + return nil +} From 8d716be896f5321a20d456541ef14baf9bfae22f Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Fri, 15 Jan 2016 21:03:35 +0800 Subject: [PATCH 12/83] Fix some go vet/lint reports --- association.go | 5 ++--- association_test.go | 6 +++--- join_table_handler.go | 6 +++--- main.go | 28 +++++++++++++--------------- main_test.go | 4 ++-- model_struct.go | 3 +-- multi_primary_keys_test.go | 2 +- pointer_test.go | 18 +++++++++--------- preload_test.go | 2 +- scope.go | 30 +++++++++++++++--------------- scope_private.go | 2 +- utils.go | 8 ++++---- 12 files changed, 55 insertions(+), 59 deletions(-) diff --git a/association.go b/association.go index 30ea36b2..e0978f2a 100644 --- a/association.go +++ b/association.go @@ -375,7 +375,7 @@ func toQueryMarks(primaryValues [][]interface{}) string { for _, primaryValue := range primaryValues { var marks []string - for _, _ = range primaryValue { + for _ = range primaryValue { marks = append(marks, "?") } @@ -396,9 +396,8 @@ func toQueryCondition(scope *Scope, columns []string) string { if len(columns) > 1 { return fmt.Sprintf("(%v)", strings.Join(newColumns, ",")) - } else { - return strings.Join(newColumns, ",") } + return strings.Join(newColumns, ",") } func toQueryValues(primaryValues [][]interface{}) (values []interface{}) { diff --git a/association_test.go b/association_test.go index ab3abd91..c2f55d0e 100644 --- a/association_test.go +++ b/association_test.go @@ -16,7 +16,7 @@ func TestBelongsTo(t *testing.T) { } if err := DB.Save(&post).Error; err != nil { - t.Errorf("Got errors when save post", err.Error()) + t.Error("Got errors when save post", err) } if post.Category.ID == 0 || post.MainCategory.ID == 0 { @@ -184,7 +184,7 @@ func TestHasOne(t *testing.T) { } if err := DB.Save(&user).Error; err != nil { - t.Errorf("Got errors when save user", err.Error()) + t.Error("Got errors when save user", err.Error()) } if user.CreditCard.UserId.Int64 == 0 { @@ -331,7 +331,7 @@ func TestHasMany(t *testing.T) { } if err := DB.Save(&post).Error; err != nil { - t.Errorf("Got errors when save post", err.Error()) + t.Error("Got errors when save post", err) } for _, comment := range post.Comments { diff --git a/join_table_handler.go b/join_table_handler.go index 006701a6..6e7f9045 100644 --- a/join_table_handler.go +++ b/join_table_handler.go @@ -173,8 +173,8 @@ func (s JoinTableHandler) JoinWith(handler JoinTableHandlerInterface, db *DB, so return db.Joins(fmt.Sprintf("INNER JOIN %v ON %v", quotedTableName, strings.Join(joinConditions, " AND "))). Where(condString, toQueryValues(foreignFieldValues)...) - } else { - db.Error = errors.New("wrong source type for join table handler") - return db } + + db.Error = errors.New("wrong source type for join table handler") + return db } diff --git a/main.go b/main.go index ed902d0b..376a967f 100644 --- a/main.go +++ b/main.go @@ -98,8 +98,8 @@ func (s *DB) New() *DB { } // NewScope create scope for callbacks, including DB's search information -func (db *DB) NewScope(value interface{}) *Scope { - dbClone := db.clone() +func (s *DB) NewScope(value interface{}) *Scope { + dbClone := s.clone() dbClone.Value = value return &Scope{db: dbClone, Search: dbClone.search.clone(), Value: value} } @@ -311,9 +311,9 @@ func (s *DB) Raw(sql string, values ...interface{}) *DB { func (s *DB) Exec(sql string, values ...interface{}) *DB { scope := s.clone().NewScope(nil) - generatedSql := scope.buildWhereCondition(map[string]interface{}{"query": sql, "args": values}) - generatedSql = strings.TrimSuffix(strings.TrimPrefix(generatedSql, "("), ")") - scope.Raw(generatedSql) + generatedSQL := scope.buildWhereCondition(map[string]interface{}{"query": sql, "args": values}) + generatedSQL = strings.TrimSuffix(strings.TrimPrefix(generatedSQL, "("), ")") + scope.Raw(generatedSQL) return scope.Exec().db } @@ -372,15 +372,16 @@ func (s *DB) RecordNotFound() bool { return s.Error == RecordNotFound } -// Migrations -func (s *DB) CreateTable(values ...interface{}) *DB { +// CreateTable create table for models +func (s *DB) CreateTable(models ...interface{}) *DB { db := s.clone() - for _, value := range values { - db = db.NewScope(value).createTable().db + for _, model := range models { + db = db.NewScope(model).createTable().db } return db } +// DropTable drop table for models func (s *DB) DropTable(values ...interface{}) *DB { db := s.clone() for _, value := range values { @@ -393,6 +394,7 @@ func (s *DB) DropTable(values ...interface{}) *DB { return db } +// DropTableIfExists drop table for models only when it exists func (s *DB) DropTableIfExists(values ...interface{}) *DB { db := s.clone() for _, value := range values { @@ -459,12 +461,8 @@ func (s *DB) CurrentDatabase() string { return name } -/* -Add foreign key to the given scope - -Example: - db.Model(&User{}).AddForeignKey("city_id", "cities(id)", "RESTRICT", "RESTRICT") -*/ +// AddForeignKey Add foreign key to the given scope +// Example: db.Model(&User{}).AddForeignKey("city_id", "cities(id)", "RESTRICT", "RESTRICT") func (s *DB) AddForeignKey(field string, dest string, onDelete string, onUpdate string) *DB { scope := s.clone().NewScope(s.Value) scope.addForeignKey(field, dest, onDelete, onUpdate) diff --git a/main_test.go b/main_test.go index 9d90bbc7..251c19aa 100644 --- a/main_test.go +++ b/main_test.go @@ -115,7 +115,7 @@ func TestSetTable(t *testing.T) { DB.Create(getPreparedUser("pluck_user3", "pluck_user")) if err := DB.Table("users").Where("role = ?", "pluck_user").Pluck("age", &[]int{}).Error; err != nil { - t.Errorf("No errors should happen if set table for pluck", err.Error()) + t.Error("No errors should happen if set table for pluck", err) } var users []User @@ -545,7 +545,7 @@ func TestTimeWithZone(t *testing.T) { DB.First(&findUser, "name = ?", name) foundBirthday = findUser.Birthday.UTC().Format(format) if foundBirthday != expectedBirthday { - t.Errorf("User's birthday should not be changed after find for name=%s, expected bday=%+v but actual value=%+v or %+v", name, expectedBirthday, foundBirthday) + t.Errorf("User's birthday should not be changed after find for name=%s, expected bday=%+v but actual value=%+v", name, expectedBirthday, foundBirthday) } if DB.Where("id = ? AND birthday >= ?", findUser.Id, user.Birthday.Add(-time.Minute)).First(&findUser2).RecordNotFound() { diff --git a/model_struct.go b/model_struct.go index d80165c8..b47f8534 100644 --- a/model_struct.go +++ b/model_struct.go @@ -560,9 +560,8 @@ func (scope *Scope) generateSqlTag(field *StructField) string { if strings.TrimSpace(additionalType) == "" { return sqlType - } else { - return fmt.Sprintf("%v %v", sqlType, additionalType) } + return fmt.Sprintf("%v %v", sqlType, additionalType) } func parseTagSetting(tags reflect.StructTag) map[string]string { diff --git a/multi_primary_keys_test.go b/multi_primary_keys_test.go index ea80326e..8b275d18 100644 --- a/multi_primary_keys_test.go +++ b/multi_primary_keys_test.go @@ -21,7 +21,7 @@ type Tag struct { ID uint `gorm:"primary_key"` Locale string `gorm:"primary_key"` Value string - Blogs []*Blog `gorm:"many2many:"blogs_tags` + Blogs []*Blog `gorm:"many2many:blogs_tags"` } func compareTags(tags []Tag, contents []string) bool { diff --git a/pointer_test.go b/pointer_test.go index b47717f3..2a68a5ab 100644 --- a/pointer_test.go +++ b/pointer_test.go @@ -39,46 +39,46 @@ func TestPointerFields(t *testing.T) { var nilPointerStruct = PointerStruct{} if err := DB.Create(&nilPointerStruct).Error; err != nil { - t.Errorf("Failed to save nil pointer struct", err) + t.Error("Failed to save nil pointer struct", err) } var pointerStruct2 PointerStruct if err := DB.First(&pointerStruct2, "id = ?", nilPointerStruct.ID).Error; err != nil { - t.Errorf("Failed to query saved nil pointer struct", err) + t.Error("Failed to query saved nil pointer struct", err) } var normalStruct2 NormalStruct if err := DB.Table(tableName).First(&normalStruct2, "id = ?", nilPointerStruct.ID).Error; err != nil { - t.Errorf("Failed to query saved nil pointer struct", err) + t.Error("Failed to query saved nil pointer struct", err) } var partialNilPointerStruct1 = PointerStruct{Num: &num} if err := DB.Create(&partialNilPointerStruct1).Error; err != nil { - t.Errorf("Failed to save partial nil pointer struct", err) + t.Error("Failed to save partial nil pointer struct", err) } var pointerStruct3 PointerStruct if err := DB.First(&pointerStruct3, "id = ?", partialNilPointerStruct1.ID).Error; err != nil || *pointerStruct3.Num != num { - t.Errorf("Failed to query saved partial nil pointer struct", err) + t.Error("Failed to query saved partial nil pointer struct", err) } var normalStruct3 NormalStruct if err := DB.Table(tableName).First(&normalStruct3, "id = ?", partialNilPointerStruct1.ID).Error; err != nil || normalStruct3.Num != num { - t.Errorf("Failed to query saved partial pointer struct", err) + t.Error("Failed to query saved partial pointer struct", err) } var partialNilPointerStruct2 = PointerStruct{Name: &name} if err := DB.Create(&partialNilPointerStruct2).Error; err != nil { - t.Errorf("Failed to save partial nil pointer struct", err) + t.Error("Failed to save partial nil pointer struct", err) } var pointerStruct4 PointerStruct if err := DB.First(&pointerStruct4, "id = ?", partialNilPointerStruct2.ID).Error; err != nil || *pointerStruct4.Name != name { - t.Errorf("Failed to query saved partial nil pointer struct", err) + t.Error("Failed to query saved partial nil pointer struct", err) } var normalStruct4 NormalStruct if err := DB.Table(tableName).First(&normalStruct4, "id = ?", partialNilPointerStruct2.ID).Error; err != nil || normalStruct4.Name != name { - t.Errorf("Failed to query saved partial pointer struct", err) + t.Error("Failed to query saved partial pointer struct", err) } } diff --git a/preload_test.go b/preload_test.go index 4f65d1d8..7f4e7d50 100644 --- a/preload_test.go +++ b/preload_test.go @@ -1133,7 +1133,7 @@ func TestNilPointerSlice(t *testing.T) { } if len(got) != 2 { - t.Error("got %v items, expected 2", len(got)) + t.Errorf("got %v items, expected 2", len(got)) } if !reflect.DeepEqual(got[0], want) && !reflect.DeepEqual(got[1], want) { diff --git a/scope.go b/scope.go index f7364e3d..0d6602b5 100644 --- a/scope.go +++ b/scope.go @@ -17,7 +17,7 @@ type Scope struct { SqlVars []interface{} db *DB indirectValue *reflect.Value - instanceId string + instanceID string primaryKeyField *Field skipLeft bool fields map[string]*Field @@ -83,9 +83,9 @@ func (scope *Scope) Quote(str string) string { newStrs = append(newStrs, scope.Dialect().Quote(str)) } return strings.Join(newStrs, ".") - } else { - return scope.Dialect().Quote(str) } + + return scope.Dialect().Quote(str) } func (scope *Scope) QuoteIfPossible(str string) string { @@ -251,10 +251,10 @@ func (scope *Scope) AddToVars(value interface{}) string { exp = strings.Replace(exp, "?", scope.AddToVars(arg), 1) } return exp - } else { - scope.SqlVars = append(scope.SqlVars, value) - return scope.Dialect().BinVar(len(scope.SqlVars)) } + + scope.SqlVars = append(scope.SqlVars, value) + return scope.Dialect().BinVar(len(scope.SqlVars)) } type tabler interface { @@ -289,9 +289,9 @@ func (scope *Scope) QuotedTableName() (name string) { return scope.Search.tableName } return scope.Quote(scope.Search.tableName) - } else { - return scope.Quote(scope.TableName()) } + + return scope.Quote(scope.TableName()) } // CombinedConditionSql get combined condition sql @@ -341,20 +341,20 @@ func (scope *Scope) Get(name string) (interface{}, bool) { return scope.db.Get(name) } -// InstanceId get InstanceId for scope -func (scope *Scope) InstanceId() string { - if scope.instanceId == "" { - scope.instanceId = fmt.Sprintf("%v%v", &scope, &scope.db) +// InstanceID get InstanceID for scope +func (scope *Scope) InstanceID() string { + if scope.instanceID == "" { + scope.instanceID = fmt.Sprintf("%v%v", &scope, &scope.db) } - return scope.instanceId + return scope.instanceID } func (scope *Scope) InstanceSet(name string, value interface{}) *Scope { - return scope.Set(name+scope.InstanceId(), value) + return scope.Set(name+scope.InstanceID(), value) } func (scope *Scope) InstanceGet(name string) (interface{}, bool) { - return scope.Get(name + scope.InstanceId()) + return scope.Get(name + scope.InstanceID()) } // Begin start a transaction diff --git a/scope_private.go b/scope_private.go index a4d53366..a9288bb2 100644 --- a/scope_private.go +++ b/scope_private.go @@ -596,7 +596,7 @@ func (scope *Scope) createJoinTable(field *StructField) { func (scope *Scope) createTable() *Scope { var tags []string var primaryKeys []string - var primaryKeyInColumnType bool = false + var primaryKeyInColumnType = false for _, field := range scope.GetStructFields() { if field.IsNormal { sqlTag := scope.generateSqlTag(field) diff --git a/utils.go b/utils.go index 158d8a3b..d47065bf 100644 --- a/utils.go +++ b/utils.go @@ -41,11 +41,11 @@ func newSafeMap() *safeMap { var smap = newSafeMap() -type Case bool +type strCase bool const ( - lower Case = false - upper Case = true + lower strCase = false + upper strCase = true ) func ToDBName(name string) string { @@ -56,7 +56,7 @@ func ToDBName(name string) string { var ( value = commonInitialismsReplacer.Replace(name) buf = bytes.NewBufferString("") - lastCase, currCase, nextCase Case + lastCase, currCase, nextCase strCase ) for i, v := range value[:len(value)-1] { From 41870191b0f767ff9c8d01a4f42fbd7f4ce48dca Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Fri, 15 Jan 2016 22:14:21 +0800 Subject: [PATCH 13/83] Refactor Association Mode --- association.go | 185 ++++--------------------------------------- association_utils.go | 158 ++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 170 deletions(-) create mode 100644 association_utils.go diff --git a/association.go b/association.go index e0978f2a..f828aea4 100644 --- a/association.go +++ b/association.go @@ -1,12 +1,11 @@ package gorm import ( - "errors" "fmt" "reflect" - "strings" ) +// Association Association Mode contains some helper methods to handle relationship things easily. type Association struct { Scope *Scope Column string @@ -14,86 +13,13 @@ type Association struct { Field *Field } -func (association *Association) setErr(err error) *Association { - if err != nil { - association.Error = err - } - return association -} - +// Find find out all related associations func (association *Association) Find(value interface{}) *Association { association.Scope.related(value, association.Column) return association.setErr(association.Scope.db.Error) } -func (association *Association) saveAssociations(values ...interface{}) *Association { - scope := association.Scope - field := association.Field - relationship := association.Field.Relationship - - saveAssociation := func(reflectValue reflect.Value) { - // value has to been pointer - if reflectValue.Kind() != reflect.Ptr { - reflectPtr := reflect.New(reflectValue.Type()) - reflectPtr.Elem().Set(reflectValue) - reflectValue = reflectPtr - } - - // value has to been saved for many2many - if relationship.Kind == "many_to_many" { - if scope.New(reflectValue.Interface()).PrimaryKeyZero() { - association.setErr(scope.NewDB().Save(reflectValue.Interface()).Error) - } - } - - // Assign Fields - var fieldType = field.Field.Type() - var setFieldBackToValue, setSliceFieldBackToValue bool - if reflectValue.Type().AssignableTo(fieldType) { - field.Set(reflectValue) - } else if reflectValue.Type().Elem().AssignableTo(fieldType) { - // if field's type is struct, then need to set value back to argument after save - setFieldBackToValue = true - field.Set(reflectValue.Elem()) - } else if fieldType.Kind() == reflect.Slice { - if reflectValue.Type().AssignableTo(fieldType.Elem()) { - field.Set(reflect.Append(field.Field, reflectValue)) - } else if reflectValue.Type().Elem().AssignableTo(fieldType.Elem()) { - // if field's type is slice of struct, then need to set value back to argument after save - setSliceFieldBackToValue = true - field.Set(reflect.Append(field.Field, reflectValue.Elem())) - } - } - - if relationship.Kind == "many_to_many" { - association.setErr(relationship.JoinTableHandler.Add(relationship.JoinTableHandler, scope.NewDB(), scope.Value, reflectValue.Interface())) - } else { - association.setErr(scope.NewDB().Select(field.Name).Save(scope.Value).Error) - - if setFieldBackToValue { - reflectValue.Elem().Set(field.Field) - } else if setSliceFieldBackToValue { - reflectValue.Elem().Set(field.Field.Index(field.Field.Len() - 1)) - } - } - } - - for _, value := range values { - reflectValue := reflect.ValueOf(value) - indirectReflectValue := reflect.Indirect(reflectValue) - if indirectReflectValue.Kind() == reflect.Struct { - saveAssociation(reflectValue) - } else if indirectReflectValue.Kind() == reflect.Slice { - for i := 0; i < indirectReflectValue.Len(); i++ { - saveAssociation(indirectReflectValue.Index(i)) - } - } else { - association.setErr(errors.New("invalid value type")) - } - } - return association -} - +// Append append new associations for many2many, has_many, will replace current association for has_one, belongs_to func (association *Association) Append(values ...interface{}) *Association { if relationship := association.Field.Relationship; relationship.Kind == "has_one" { return association.Replace(values...) @@ -101,6 +27,7 @@ func (association *Association) Append(values ...interface{}) *Association { return association.saveAssociations(values...) } +// Replace replace current associations with new one func (association *Association) Replace(values ...interface{}) *Association { var ( relationship = association.Field.Relationship @@ -115,7 +42,7 @@ func (association *Association) Replace(values ...interface{}) *Association { // Belongs To if relationship.Kind == "belongs_to" { - // Set foreign key to be null only when clearing value + // Set foreign key to be null when clearing value (length equals 0) if len(values) == 0 { // Set foreign key to be nil var foreignKeyMap = map[string]interface{}{} @@ -125,29 +52,21 @@ func (association *Association) Replace(values ...interface{}) *Association { association.setErr(newDB.Model(scope.Value).UpdateColumn(foreignKeyMap).Error) } } else { - // Relations + // Polymorphic Relations if relationship.PolymorphicDBName != "" { newDB = newDB.Where(fmt.Sprintf("%v = ?", scope.Quote(relationship.PolymorphicDBName)), scope.TableName()) } // Relations except new created if len(values) > 0 { - var newPrimaryKeys [][]interface{} var associationForeignFieldNames []string - if relationship.Kind == "many_to_many" { - // If many to many relations, get it from foreign key associationForeignFieldNames = relationship.AssociationForeignFieldNames } else { - // If other relations, get real primary keys - for _, field := range scope.New(reflect.New(field.Type()).Interface()).Fields() { - if field.IsPrimaryKey { - associationForeignFieldNames = append(associationForeignFieldNames, field.Name) - } - } + associationForeignFieldNames = relationship.AssociationForeignDBNames } - newPrimaryKeys = association.getPrimaryKeys(associationForeignFieldNames, field.Interface()) + newPrimaryKeys := association.getPrimaryKeys(associationForeignFieldNames, field.Interface()) if len(newPrimaryKeys) > 0 { sql := fmt.Sprintf("%v NOT IN (%v)", toQueryCondition(scope, relationship.AssociationForeignDBNames), toQueryMarks(newPrimaryKeys)) @@ -156,13 +75,11 @@ func (association *Association) Replace(values ...interface{}) *Association { } if relationship.Kind == "many_to_many" { - for idx, foreignKey := range relationship.ForeignDBNames { - if field, ok := scope.FieldByName(relationship.ForeignFieldNames[idx]); ok { - newDB = newDB.Where(fmt.Sprintf("%v = ?", scope.Quote(foreignKey)), field.Field.Interface()) - } - } + if sourcePrimaryKeys := association.getPrimaryKeys(relationship.ForeignFieldNames, scope.Value); len(sourcePrimaryKeys) > 0 { + newDB = newDB.Where(fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relationship.ForeignDBNames), toQueryMarks(sourcePrimaryKeys)), toQueryValues(sourcePrimaryKeys)...) - association.setErr(relationship.JoinTableHandler.Delete(relationship.JoinTableHandler, newDB, relationship)) + association.setErr(relationship.JoinTableHandler.Delete(relationship.JoinTableHandler, newDB, relationship)) + } } else if relationship.Kind == "has_one" || relationship.Kind == "has_many" { var foreignKeyMap = map[string]interface{}{} for idx, foreignKey := range relationship.ForeignDBNames { @@ -179,6 +96,7 @@ func (association *Association) Replace(values ...interface{}) *Association { return association } +// Delete remove relationship between source & passed arguments, but won't delete those arguments func (association *Association) Delete(values ...interface{}) *Association { var ( relationship = association.Field.Relationship @@ -292,10 +210,12 @@ func (association *Association) Delete(values ...interface{}) *Association { return association } +// Clear remove relationship between source & current associations, won't delete those associations func (association *Association) Clear() *Association { return association.Replace() } +// Count return the count of current associations func (association *Association) Count() int { var ( count = 0 @@ -333,78 +253,3 @@ func (association *Association) Count() int { return count } - -func (association *Association) getPrimaryKeys(columns []string, values ...interface{}) (results [][]interface{}) { - scope := association.Scope - - for _, value := range values { - reflectValue := reflect.Indirect(reflect.ValueOf(value)) - if reflectValue.Kind() == reflect.Slice { - for i := 0; i < reflectValue.Len(); i++ { - primaryKeys := []interface{}{} - newScope := scope.New(reflectValue.Index(i).Interface()) - for _, column := range columns { - if field, ok := newScope.FieldByName(column); ok { - primaryKeys = append(primaryKeys, field.Field.Interface()) - } else { - primaryKeys = append(primaryKeys, "") - } - } - results = append(results, primaryKeys) - } - } else if reflectValue.Kind() == reflect.Struct { - newScope := scope.New(value) - var primaryKeys []interface{} - for _, column := range columns { - if field, ok := newScope.FieldByName(column); ok { - primaryKeys = append(primaryKeys, field.Field.Interface()) - } else { - primaryKeys = append(primaryKeys, "") - } - } - - results = append(results, primaryKeys) - } - } - - return -} - -func toQueryMarks(primaryValues [][]interface{}) string { - var results []string - - for _, primaryValue := range primaryValues { - var marks []string - for _ = range primaryValue { - marks = append(marks, "?") - } - - if len(marks) > 1 { - results = append(results, fmt.Sprintf("(%v)", strings.Join(marks, ","))) - } else { - results = append(results, strings.Join(marks, "")) - } - } - return strings.Join(results, ",") -} - -func toQueryCondition(scope *Scope, columns []string) string { - var newColumns []string - for _, column := range columns { - newColumns = append(newColumns, scope.Quote(column)) - } - - if len(columns) > 1 { - return fmt.Sprintf("(%v)", strings.Join(newColumns, ",")) - } - return strings.Join(newColumns, ",") -} - -func toQueryValues(primaryValues [][]interface{}) (values []interface{}) { - for _, primaryValue := range primaryValues { - for _, value := range primaryValue { - values = append(values, value) - } - } - return values -} diff --git a/association_utils.go b/association_utils.go new file mode 100644 index 00000000..7ec2ab7f --- /dev/null +++ b/association_utils.go @@ -0,0 +1,158 @@ +package gorm + +import ( + "errors" + "fmt" + "reflect" + "strings" +) + +func (association *Association) setErr(err error) *Association { + if err != nil { + association.Error = err + } + return association +} + +func (association *Association) saveAssociations(values ...interface{}) *Association { + scope := association.Scope + field := association.Field + relationship := association.Field.Relationship + + saveAssociation := func(reflectValue reflect.Value) { + // value has to been pointer + if reflectValue.Kind() != reflect.Ptr { + reflectPtr := reflect.New(reflectValue.Type()) + reflectPtr.Elem().Set(reflectValue) + reflectValue = reflectPtr + } + + // value has to been saved for many2many + if relationship.Kind == "many_to_many" { + if scope.New(reflectValue.Interface()).PrimaryKeyZero() { + association.setErr(scope.NewDB().Save(reflectValue.Interface()).Error) + } + } + + // Assign Fields + var fieldType = field.Field.Type() + var setFieldBackToValue, setSliceFieldBackToValue bool + if reflectValue.Type().AssignableTo(fieldType) { + field.Set(reflectValue) + } else if reflectValue.Type().Elem().AssignableTo(fieldType) { + // if field's type is struct, then need to set value back to argument after save + setFieldBackToValue = true + field.Set(reflectValue.Elem()) + } else if fieldType.Kind() == reflect.Slice { + if reflectValue.Type().AssignableTo(fieldType.Elem()) { + field.Set(reflect.Append(field.Field, reflectValue)) + } else if reflectValue.Type().Elem().AssignableTo(fieldType.Elem()) { + // if field's type is slice of struct, then need to set value back to argument after save + setSliceFieldBackToValue = true + field.Set(reflect.Append(field.Field, reflectValue.Elem())) + } + } + + if relationship.Kind == "many_to_many" { + association.setErr(relationship.JoinTableHandler.Add(relationship.JoinTableHandler, scope.NewDB(), scope.Value, reflectValue.Interface())) + } else { + association.setErr(scope.NewDB().Select(field.Name).Save(scope.Value).Error) + + if setFieldBackToValue { + reflectValue.Elem().Set(field.Field) + } else if setSliceFieldBackToValue { + reflectValue.Elem().Set(field.Field.Index(field.Field.Len() - 1)) + } + } + } + + for _, value := range values { + reflectValue := reflect.ValueOf(value) + indirectReflectValue := reflect.Indirect(reflectValue) + if indirectReflectValue.Kind() == reflect.Struct { + saveAssociation(reflectValue) + } else if indirectReflectValue.Kind() == reflect.Slice { + for i := 0; i < indirectReflectValue.Len(); i++ { + saveAssociation(indirectReflectValue.Index(i)) + } + } else { + association.setErr(errors.New("invalid value type")) + } + } + return association +} + +func (association *Association) getPrimaryKeys(columns []string, values ...interface{}) (results [][]interface{}) { + scope := association.Scope + + for _, value := range values { + reflectValue := reflect.Indirect(reflect.ValueOf(value)) + if reflectValue.Kind() == reflect.Slice { + for i := 0; i < reflectValue.Len(); i++ { + primaryKeys := []interface{}{} + newScope := scope.New(reflectValue.Index(i).Interface()) + for _, column := range columns { + if field, ok := newScope.FieldByName(column); ok { + primaryKeys = append(primaryKeys, field.Field.Interface()) + } else { + primaryKeys = append(primaryKeys, "") + } + } + results = append(results, primaryKeys) + } + } else if reflectValue.Kind() == reflect.Struct { + newScope := scope.New(value) + var primaryKeys []interface{} + for _, column := range columns { + if field, ok := newScope.FieldByName(column); ok { + primaryKeys = append(primaryKeys, field.Field.Interface()) + } else { + primaryKeys = append(primaryKeys, "") + } + } + + results = append(results, primaryKeys) + } + } + + return +} + +func toQueryMarks(primaryValues [][]interface{}) string { + var results []string + + for _, primaryValue := range primaryValues { + var marks []string + for _ = range primaryValue { + marks = append(marks, "?") + } + + if len(marks) > 1 { + results = append(results, fmt.Sprintf("(%v)", strings.Join(marks, ","))) + } else { + results = append(results, strings.Join(marks, "")) + } + } + return strings.Join(results, ",") +} + +func toQueryCondition(scope *Scope, columns []string) string { + var newColumns []string + for _, column := range columns { + newColumns = append(newColumns, scope.Quote(column)) + } + + if len(columns) > 1 { + return fmt.Sprintf("(%v)", strings.Join(newColumns, ",")) + } + return strings.Join(newColumns, ",") +} + +func toQueryValues(primaryValues [][]interface{}) (values []interface{}) { + for _, primaryValue := range primaryValues { + for _, value := range primaryValue { + values = append(values, value) + } + } + return values +} From 822e895d4d0707402b04a90ff7185846ce53ebac Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Fri, 15 Jan 2016 22:53:09 +0800 Subject: [PATCH 14/83] Refactor getColumnAsArray --- association.go | 6 +++--- join_table_handler.go | 4 ++-- preload.go | 6 +++--- scope_utils.go | 32 +++++++++++++++++++------------- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/association.go b/association.go index f828aea4..ecf6eb49 100644 --- a/association.go +++ b/association.go @@ -117,7 +117,7 @@ func (association *Association) Delete(values ...interface{}) *Association { } } - deletingPrimaryKeys := association.getPrimaryKeys(deletingResourcePrimaryFieldNames, values...) + deletingPrimaryKeys := scope.getColumnAsArray(deletingResourcePrimaryFieldNames, values...) if relationship.Kind == "many_to_many" { // source value's foreign keys @@ -141,7 +141,7 @@ func (association *Association) Delete(values ...interface{}) *Association { if relationship.Kind == "belongs_to" { // find with deleting relation's foreign keys - primaryKeys := association.getPrimaryKeys(relationship.AssociationForeignFieldNames, values...) + primaryKeys := scope.getColumnAsArray(relationship.AssociationForeignFieldNames, values...) newDB = newDB.Where( fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relationship.ForeignDBNames), toQueryMarks(primaryKeys)), toQueryValues(primaryKeys)..., @@ -158,7 +158,7 @@ func (association *Association) Delete(values ...interface{}) *Association { } } else if relationship.Kind == "has_one" || relationship.Kind == "has_many" { // find all relations - primaryKeys := association.getPrimaryKeys(relationship.AssociationForeignFieldNames, scope.Value) + primaryKeys := scope.getColumnAsArray(relationship.AssociationForeignFieldNames, scope.Value) newDB = newDB.Where( fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relationship.ForeignDBNames), toQueryMarks(primaryKeys)), toQueryValues(primaryKeys)..., diff --git a/join_table_handler.go b/join_table_handler.go index 6e7f9045..9e6c027a 100644 --- a/join_table_handler.go +++ b/join_table_handler.go @@ -154,7 +154,7 @@ func (s JoinTableHandler) JoinWith(handler JoinTableHandlerInterface, db *DB, so foreignFieldNames = append(foreignFieldNames, scope.Fields()[foreignKey.AssociationDBName].Name) } - foreignFieldValues := scope.getColumnAsArray(foreignFieldNames) + foreignFieldValues := scope.getColumnAsArray(foreignFieldNames, scope.Value) var condString string if len(foreignFieldValues) > 0 { @@ -165,7 +165,7 @@ func (s JoinTableHandler) JoinWith(handler JoinTableHandlerInterface, db *DB, so condString = fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, quotedForeignDBNames), toQueryMarks(foreignFieldValues)) - keys := scope.getColumnAsArray(foreignFieldNames) + keys := scope.getColumnAsArray(foreignFieldNames, scope.Value) values = append(values, toQueryValues(keys)) } else { condString = fmt.Sprintf("1 <> 1") diff --git a/preload.go b/preload.go index 2333cade..20c9aacf 100644 --- a/preload.go +++ b/preload.go @@ -77,7 +77,7 @@ func (scope *Scope) handleHasOnePreload(field *Field, conditions []interface{}) relation := field.Relationship // get relations's primary keys - primaryKeys := scope.getColumnAsArray(relation.AssociationForeignFieldNames) + primaryKeys := scope.getColumnAsArray(relation.AssociationForeignFieldNames, scope.Value) if len(primaryKeys) == 0 { return } @@ -112,7 +112,7 @@ func (scope *Scope) handleHasManyPreload(field *Field, conditions []interface{}) relation := field.Relationship // get relations's primary keys - primaryKeys := scope.getColumnAsArray(relation.AssociationForeignFieldNames) + primaryKeys := scope.getColumnAsArray(relation.AssociationForeignFieldNames, scope.Value) if len(primaryKeys) == 0 { return } @@ -149,7 +149,7 @@ func (scope *Scope) handleBelongsToPreload(field *Field, conditions []interface{ relation := field.Relationship // get relations's primary keys - primaryKeys := scope.getColumnAsArray(relation.ForeignFieldNames) + primaryKeys := scope.getColumnAsArray(relation.ForeignFieldNames, scope.Value) if len(primaryKeys) == 0 { return } diff --git a/scope_utils.go b/scope_utils.go index 99957adc..ffaa99b4 100644 --- a/scope_utils.go +++ b/scope_utils.go @@ -2,24 +2,30 @@ package gorm import "reflect" -func (scope *Scope) getColumnAsArray(columns []string) (results [][]interface{}) { - indirectScopeValue := scope.IndirectValue() - switch indirectScopeValue.Kind() { - case reflect.Slice: - for i := 0; i < indirectScopeValue.Len(); i++ { +func (scope *Scope) getColumnAsArray(columns []string, values ...interface{}) (results [][]interface{}) { + for _, value := range values { + indirectValue := reflect.ValueOf(value) + for indirectValue.Kind() == reflect.Ptr { + indirectValue = indirectValue.Elem() + } + + switch indirectValue.Kind() { + case reflect.Slice: + for i := 0; i < indirectValue.Len(); i++ { + var result []interface{} + var object = reflect.Indirect(indirectValue.Index(i)) + for _, column := range columns { + result = append(result, object.FieldByName(column).Interface()) + } + results = append(results, result) + } + case reflect.Struct: var result []interface{} - var object = reflect.Indirect(indirectScopeValue.Index(i)) for _, column := range columns { - result = append(result, object.FieldByName(column).Interface()) + result = append(result, indirectValue.FieldByName(column).Interface()) } results = append(results, result) } - case reflect.Struct: - var result []interface{} - for _, column := range columns { - result = append(result, indirectScopeValue.FieldByName(column).Interface()) - } - return [][]interface{}{result} } return } From 67874f923242e53af66e16619357ea7d0bf63415 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sat, 16 Jan 2016 06:00:18 +0800 Subject: [PATCH 15/83] Keep Refactoring Association Mode --- association.go | 104 ++++++++++++++++++++++++++----------------- association_utils.go | 36 --------------- 2 files changed, 64 insertions(+), 76 deletions(-) diff --git a/association.go b/association.go index ecf6eb49..862276d5 100644 --- a/association.go +++ b/association.go @@ -57,16 +57,23 @@ func (association *Association) Replace(values ...interface{}) *Association { newDB = newDB.Where(fmt.Sprintf("%v = ?", scope.Quote(relationship.PolymorphicDBName)), scope.TableName()) } - // Relations except new created + // Delete Relations except new created if len(values) > 0 { var associationForeignFieldNames []string if relationship.Kind == "many_to_many" { - associationForeignFieldNames = relationship.AssociationForeignFieldNames + // if many to many relations, get association fields name from association foreign keys + associationFields := scope.New(reflect.New(field.Type()).Interface()).Fields() + for _, dbName := range relationship.AssociationForeignFieldNames { + associationForeignFieldNames = append(associationForeignFieldNames, associationFields[dbName].Name) + } } else { - associationForeignFieldNames = relationship.AssociationForeignDBNames + // If other relations, use primary keys + for _, field := range scope.New(reflect.New(field.Type()).Interface()).PrimaryFields() { + associationForeignFieldNames = append(associationForeignFieldNames, field.Name) + } } - newPrimaryKeys := association.getPrimaryKeys(associationForeignFieldNames, field.Interface()) + newPrimaryKeys := scope.getColumnAsArray(associationForeignFieldNames, field.Interface()) if len(newPrimaryKeys) > 0 { sql := fmt.Sprintf("%v NOT IN (%v)", toQueryCondition(scope, relationship.AssociationForeignDBNames), toQueryMarks(newPrimaryKeys)) @@ -75,12 +82,25 @@ func (association *Association) Replace(values ...interface{}) *Association { } if relationship.Kind == "many_to_many" { - if sourcePrimaryKeys := association.getPrimaryKeys(relationship.ForeignFieldNames, scope.Value); len(sourcePrimaryKeys) > 0 { + // if many to many relations, delete related relations from join table + + // get source fields name from source foreign keys + var ( + sourceFields = scope.Fields() + sourceForeignFieldNames []string + ) + + for _, dbName := range relationship.ForeignFieldNames { + sourceForeignFieldNames = append(sourceForeignFieldNames, sourceFields[dbName].Name) + } + + if sourcePrimaryKeys := scope.getColumnAsArray(sourceForeignFieldNames, scope.Value); len(sourcePrimaryKeys) > 0 { newDB = newDB.Where(fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relationship.ForeignDBNames), toQueryMarks(sourcePrimaryKeys)), toQueryValues(sourcePrimaryKeys)...) association.setErr(relationship.JoinTableHandler.Delete(relationship.JoinTableHandler, newDB, relationship)) } } else if relationship.Kind == "has_one" || relationship.Kind == "has_many" { + // has_one or has_many relations, set foreign key to be nil (TODO or delete them?) var foreignKeyMap = map[string]interface{}{} for idx, foreignKey := range relationship.ForeignDBNames { foreignKeyMap[foreignKey] = nil @@ -110,11 +130,9 @@ func (association *Association) Delete(values ...interface{}) *Association { } var deletingResourcePrimaryFieldNames, deletingResourcePrimaryDBNames []string - for _, field := range scope.New(reflect.New(field.Type()).Interface()).Fields() { - if field.IsPrimaryKey { - deletingResourcePrimaryFieldNames = append(deletingResourcePrimaryFieldNames, field.Name) - deletingResourcePrimaryDBNames = append(deletingResourcePrimaryDBNames, field.DBName) - } + for _, field := range scope.New(reflect.New(field.Type()).Interface()).PrimaryFields() { + deletingResourcePrimaryFieldNames = append(deletingResourcePrimaryFieldNames, field.Name) + deletingResourcePrimaryDBNames = append(deletingResourcePrimaryDBNames, field.DBName) } deletingPrimaryKeys := scope.getColumnAsArray(deletingResourcePrimaryFieldNames, values...) @@ -127,8 +145,15 @@ func (association *Association) Delete(values ...interface{}) *Association { } } + // get association's foreign fields name + var associationFields = scope.New(reflect.New(field.Type()).Interface()).Fields() + var associationForeignFieldNames []string + for _, associationDBName := range relationship.AssociationForeignFieldNames { + associationForeignFieldNames = append(associationForeignFieldNames, associationFields[associationDBName].Name) + } + // association value's foreign keys - deletingPrimaryKeys := association.getPrimaryKeys(relationship.AssociationForeignFieldNames, values...) + deletingPrimaryKeys := scope.getColumnAsArray(associationForeignFieldNames, values...) sql := fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relationship.AssociationForeignDBNames), toQueryMarks(deletingPrimaryKeys)) newDB = newDB.Where(sql, toQueryValues(deletingPrimaryKeys)...) @@ -147,7 +172,7 @@ func (association *Association) Delete(values ...interface{}) *Association { toQueryValues(primaryKeys)..., ) - // set foreign key to be null + // set foreign key to be null if there are some records affected modelValue := reflect.New(scope.GetModelStruct().ModelType).Interface() if results := newDB.Model(modelValue).UpdateColumn(foreignKeyMap); results.Error == nil { if results.RowsAffected > 0 { @@ -176,28 +201,29 @@ func (association *Association) Delete(values ...interface{}) *Association { } } - // Remove deleted records from field + // Remove deleted records from source's field if association.Error == nil { if association.Field.Field.Kind() == reflect.Slice { leftValues := reflect.Zero(association.Field.Field.Type()) for i := 0; i < association.Field.Field.Len(); i++ { reflectValue := association.Field.Field.Index(i) - primaryKey := association.getPrimaryKeys(deletingResourcePrimaryFieldNames, reflectValue.Interface())[0] - var included = false + primaryKey := scope.getColumnAsArray(deletingResourcePrimaryFieldNames, reflectValue.Interface())[0] + var isDeleted = false for _, pk := range deletingPrimaryKeys { if equalAsString(primaryKey, pk) { - included = true + isDeleted = true + break } } - if !included { + if !isDeleted { leftValues = reflect.Append(leftValues, reflectValue) } } association.Field.Set(leftValues) } else if association.Field.Field.Kind() == reflect.Struct { - primaryKey := association.getPrimaryKeys(deletingResourcePrimaryFieldNames, association.Field.Field.Interface())[0] + primaryKey := scope.getColumnAsArray(deletingResourcePrimaryFieldNames, association.Field.Field.Interface())[0] for _, pk := range deletingPrimaryKeys { if equalAsString(primaryKey, pk) { association.Field.Set(reflect.Zero(association.Field.Field.Type())) @@ -222,34 +248,32 @@ func (association *Association) Count() int { relationship = association.Field.Relationship scope = association.Scope fieldValue = association.Field.Field.Interface() - newScope = scope.New(fieldValue) + query = scope.DB() ) if relationship.Kind == "many_to_many" { - relationship.JoinTableHandler.JoinWith(relationship.JoinTableHandler, scope.DB(), association.Scope.Value).Model(fieldValue).Count(&count) + query = relationship.JoinTableHandler.JoinWith(relationship.JoinTableHandler, scope.DB(), association.Scope.Value) } else if relationship.Kind == "has_many" || relationship.Kind == "has_one" { - query := scope.DB() - for idx, foreignKey := range relationship.ForeignDBNames { - if field, ok := scope.FieldByName(relationship.AssociationForeignDBNames[idx]); ok { - query = query.Where(fmt.Sprintf("%v.%v = ?", newScope.QuotedTableName(), scope.Quote(foreignKey)), - field.Field.Interface()) - } - } - - if relationship.PolymorphicType != "" { - query = query.Where(fmt.Sprintf("%v.%v = ?", newScope.QuotedTableName(), newScope.Quote(relationship.PolymorphicDBName)), scope.TableName()) - } - query.Model(fieldValue).Count(&count) + primaryKeys := scope.getColumnAsArray(relationship.AssociationForeignFieldNames, scope.Value) + query = query.Where( + fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relationship.ForeignDBNames), toQueryMarks(primaryKeys)), + toQueryValues(primaryKeys)..., + ) } else if relationship.Kind == "belongs_to" { - query := scope.DB() - for idx, primaryKey := range relationship.AssociationForeignDBNames { - if field, ok := scope.FieldByName(relationship.ForeignDBNames[idx]); ok { - query = query.Where(fmt.Sprintf("%v.%v = ?", newScope.QuotedTableName(), scope.Quote(primaryKey)), - field.Field.Interface()) - } - } - query.Model(fieldValue).Count(&count) + primaryKeys := scope.getColumnAsArray(relationship.ForeignFieldNames, scope.Value) + query = query.Where( + fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relationship.AssociationForeignDBNames), toQueryMarks(primaryKeys)), + toQueryValues(primaryKeys)..., + ) } + if relationship.PolymorphicType != "" { + query = query.Where( + fmt.Sprintf("%v.%v = ?", scope.New(fieldValue).QuotedTableName(), scope.Quote(relationship.PolymorphicDBName)), + scope.TableName(), + ) + } + + query.Model(fieldValue).Count(&count) return count } diff --git a/association_utils.go b/association_utils.go index 7ec2ab7f..912c9ca4 100644 --- a/association_utils.go +++ b/association_utils.go @@ -82,42 +82,6 @@ func (association *Association) saveAssociations(values ...interface{}) *Associa return association } -func (association *Association) getPrimaryKeys(columns []string, values ...interface{}) (results [][]interface{}) { - scope := association.Scope - - for _, value := range values { - reflectValue := reflect.Indirect(reflect.ValueOf(value)) - if reflectValue.Kind() == reflect.Slice { - for i := 0; i < reflectValue.Len(); i++ { - primaryKeys := []interface{}{} - newScope := scope.New(reflectValue.Index(i).Interface()) - for _, column := range columns { - if field, ok := newScope.FieldByName(column); ok { - primaryKeys = append(primaryKeys, field.Field.Interface()) - } else { - primaryKeys = append(primaryKeys, "") - } - } - results = append(results, primaryKeys) - } - } else if reflectValue.Kind() == reflect.Struct { - newScope := scope.New(value) - var primaryKeys []interface{} - for _, column := range columns { - if field, ok := newScope.FieldByName(column); ok { - primaryKeys = append(primaryKeys, field.Field.Interface()) - } else { - primaryKeys = append(primaryKeys, "") - } - } - - results = append(results, primaryKeys) - } - } - - return -} - func toQueryMarks(primaryValues [][]interface{}) string { var results []string From 4351917c45b202f389f3efe0a1e2bd3493d2848a Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sat, 16 Jan 2016 09:40:13 +0800 Subject: [PATCH 16/83] Don't panic with ToDbName --- utils.go | 4 ++++ utils_test.go | 1 + 2 files changed, 5 insertions(+) diff --git a/utils.go b/utils.go index d47065bf..a4cf0b8c 100644 --- a/utils.go +++ b/utils.go @@ -53,6 +53,10 @@ func ToDBName(name string) string { return v } + if name == "" { + return "" + } + var ( value = commonInitialismsReplacer.Replace(name) buf = bytes.NewBufferString("") diff --git a/utils_test.go b/utils_test.go index 81b8ce9d..0e88a8b7 100644 --- a/utils_test.go +++ b/utils_test.go @@ -8,6 +8,7 @@ import ( func TestToDBNameGenerateFriendlyName(t *testing.T) { var maps = map[string]string{ + "": "", "ThisIsATest": "this_is_a_test", "PFAndESI": "pf_and_esi", "AbcAndJkl": "abc_and_jkl", From dc23ae63bfda516527a40efb16d98c29d67c28c8 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sat, 16 Jan 2016 12:18:04 +0800 Subject: [PATCH 17/83] Keep refactoring association mode --- association.go | 137 ++++++++++++++++++++++++++++++++++--------- association_utils.go | 122 -------------------------------------- main.go | 2 +- utils.go | 40 +++++++++++++ 4 files changed, 149 insertions(+), 152 deletions(-) delete mode 100644 association_utils.go diff --git a/association.go b/association.go index 862276d5..d1984229 100644 --- a/association.go +++ b/association.go @@ -1,27 +1,28 @@ package gorm import ( + "errors" "fmt" "reflect" ) -// Association Association Mode contains some helper methods to handle relationship things easily. +// Association Mode contains some helper methods to handle relationship things easily. type Association struct { - Scope *Scope - Column string Error error - Field *Field + scope *Scope + column string + field *Field } // Find find out all related associations func (association *Association) Find(value interface{}) *Association { - association.Scope.related(value, association.Column) - return association.setErr(association.Scope.db.Error) + association.scope.related(value, association.column) + return association.setErr(association.scope.db.Error) } -// Append append new associations for many2many, has_many, will replace current association for has_one, belongs_to +// Append append new associations for many2many, has_many, replace current association for has_one, belongs_to func (association *Association) Append(values ...interface{}) *Association { - if relationship := association.Field.Relationship; relationship.Kind == "has_one" { + if relationship := association.field.Relationship; relationship.Kind == "has_one" { return association.Replace(values...) } return association.saveAssociations(values...) @@ -30,14 +31,14 @@ func (association *Association) Append(values ...interface{}) *Association { // Replace replace current associations with new one func (association *Association) Replace(values ...interface{}) *Association { var ( - relationship = association.Field.Relationship - scope = association.Scope - field = association.Field.Field + relationship = association.field.Relationship + scope = association.scope + field = association.field.Field newDB = scope.NewDB() ) // Append new values - association.Field.Set(reflect.Zero(association.Field.Field.Type())) + association.field.Set(reflect.Zero(association.field.Field.Type())) association.saveAssociations(values...) // Belongs To @@ -109,7 +110,7 @@ func (association *Association) Replace(values ...interface{}) *Association { } } - fieldValue := reflect.New(association.Field.Field.Type()).Interface() + fieldValue := reflect.New(association.field.Field.Type()).Interface() association.setErr(newDB.Model(fieldValue).UpdateColumn(foreignKeyMap).Error) } } @@ -119,9 +120,9 @@ func (association *Association) Replace(values ...interface{}) *Association { // Delete remove relationship between source & passed arguments, but won't delete those arguments func (association *Association) Delete(values ...interface{}) *Association { var ( - relationship = association.Field.Relationship - scope = association.Scope - field = association.Field.Field + relationship = association.field.Relationship + scope = association.scope + field = association.field.Field newDB = scope.NewDB() ) @@ -196,18 +197,18 @@ func (association *Association) Delete(values ...interface{}) *Association { ) // set matched relation's foreign key to be null - fieldValue := reflect.New(association.Field.Field.Type()).Interface() + fieldValue := reflect.New(association.field.Field.Type()).Interface() association.setErr(newDB.Model(fieldValue).UpdateColumn(foreignKeyMap).Error) } } // Remove deleted records from source's field if association.Error == nil { - if association.Field.Field.Kind() == reflect.Slice { - leftValues := reflect.Zero(association.Field.Field.Type()) + if field.Kind() == reflect.Slice { + leftValues := reflect.Zero(field.Type()) - for i := 0; i < association.Field.Field.Len(); i++ { - reflectValue := association.Field.Field.Index(i) + for i := 0; i < field.Len(); i++ { + reflectValue := field.Index(i) primaryKey := scope.getColumnAsArray(deletingResourcePrimaryFieldNames, reflectValue.Interface())[0] var isDeleted = false for _, pk := range deletingPrimaryKeys { @@ -221,12 +222,12 @@ func (association *Association) Delete(values ...interface{}) *Association { } } - association.Field.Set(leftValues) - } else if association.Field.Field.Kind() == reflect.Struct { - primaryKey := scope.getColumnAsArray(deletingResourcePrimaryFieldNames, association.Field.Field.Interface())[0] + association.field.Set(leftValues) + } else if field.Kind() == reflect.Struct { + primaryKey := scope.getColumnAsArray(deletingResourcePrimaryFieldNames, field.Interface())[0] for _, pk := range deletingPrimaryKeys { if equalAsString(primaryKey, pk) { - association.Field.Set(reflect.Zero(association.Field.Field.Type())) + association.field.Set(reflect.Zero(field.Type())) break } } @@ -245,14 +246,14 @@ func (association *Association) Clear() *Association { func (association *Association) Count() int { var ( count = 0 - relationship = association.Field.Relationship - scope = association.Scope - fieldValue = association.Field.Field.Interface() + relationship = association.field.Relationship + scope = association.scope + fieldValue = association.field.Field.Interface() query = scope.DB() ) if relationship.Kind == "many_to_many" { - query = relationship.JoinTableHandler.JoinWith(relationship.JoinTableHandler, scope.DB(), association.Scope.Value) + query = relationship.JoinTableHandler.JoinWith(relationship.JoinTableHandler, query, scope.Value) } else if relationship.Kind == "has_many" || relationship.Kind == "has_one" { primaryKeys := scope.getColumnAsArray(relationship.AssociationForeignFieldNames, scope.Value) query = query.Where( @@ -277,3 +278,81 @@ func (association *Association) Count() int { query.Model(fieldValue).Count(&count) return count } + +// saveAssociations save passed values as associations +func (association *Association) saveAssociations(values ...interface{}) *Association { + var ( + scope = association.scope + field = association.field + relationship = field.Relationship + ) + + saveAssociation := func(reflectValue reflect.Value) { + // value has to been pointer + if reflectValue.Kind() != reflect.Ptr { + reflectPtr := reflect.New(reflectValue.Type()) + reflectPtr.Elem().Set(reflectValue) + reflectValue = reflectPtr + } + + // value has to been saved for many2many + if relationship.Kind == "many_to_many" { + if scope.New(reflectValue.Interface()).PrimaryKeyZero() { + association.setErr(scope.NewDB().Save(reflectValue.Interface()).Error) + } + } + + // Assign Fields + var fieldType = field.Field.Type() + var setFieldBackToValue, setSliceFieldBackToValue bool + if reflectValue.Type().AssignableTo(fieldType) { + field.Set(reflectValue) + } else if reflectValue.Type().Elem().AssignableTo(fieldType) { + // if field's type is struct, then need to set value back to argument after save + setFieldBackToValue = true + field.Set(reflectValue.Elem()) + } else if fieldType.Kind() == reflect.Slice { + if reflectValue.Type().AssignableTo(fieldType.Elem()) { + field.Set(reflect.Append(field.Field, reflectValue)) + } else if reflectValue.Type().Elem().AssignableTo(fieldType.Elem()) { + // if field's type is slice of struct, then need to set value back to argument after save + setSliceFieldBackToValue = true + field.Set(reflect.Append(field.Field, reflectValue.Elem())) + } + } + + if relationship.Kind == "many_to_many" { + association.setErr(relationship.JoinTableHandler.Add(relationship.JoinTableHandler, scope.NewDB(), scope.Value, reflectValue.Interface())) + } else { + association.setErr(scope.NewDB().Select(field.Name).Save(scope.Value).Error) + + if setFieldBackToValue { + reflectValue.Elem().Set(field.Field) + } else if setSliceFieldBackToValue { + reflectValue.Elem().Set(field.Field.Index(field.Field.Len() - 1)) + } + } + } + + for _, value := range values { + reflectValue := reflect.ValueOf(value) + indirectReflectValue := reflect.Indirect(reflectValue) + if indirectReflectValue.Kind() == reflect.Struct { + saveAssociation(reflectValue) + } else if indirectReflectValue.Kind() == reflect.Slice { + for i := 0; i < indirectReflectValue.Len(); i++ { + saveAssociation(indirectReflectValue.Index(i)) + } + } else { + association.setErr(errors.New("invalid value type")) + } + } + return association +} + +func (association *Association) setErr(err error) *Association { + if err != nil { + association.Error = err + } + return association +} diff --git a/association_utils.go b/association_utils.go deleted file mode 100644 index 912c9ca4..00000000 --- a/association_utils.go +++ /dev/null @@ -1,122 +0,0 @@ -package gorm - -import ( - "errors" - "fmt" - "reflect" - "strings" -) - -func (association *Association) setErr(err error) *Association { - if err != nil { - association.Error = err - } - return association -} - -func (association *Association) saveAssociations(values ...interface{}) *Association { - scope := association.Scope - field := association.Field - relationship := association.Field.Relationship - - saveAssociation := func(reflectValue reflect.Value) { - // value has to been pointer - if reflectValue.Kind() != reflect.Ptr { - reflectPtr := reflect.New(reflectValue.Type()) - reflectPtr.Elem().Set(reflectValue) - reflectValue = reflectPtr - } - - // value has to been saved for many2many - if relationship.Kind == "many_to_many" { - if scope.New(reflectValue.Interface()).PrimaryKeyZero() { - association.setErr(scope.NewDB().Save(reflectValue.Interface()).Error) - } - } - - // Assign Fields - var fieldType = field.Field.Type() - var setFieldBackToValue, setSliceFieldBackToValue bool - if reflectValue.Type().AssignableTo(fieldType) { - field.Set(reflectValue) - } else if reflectValue.Type().Elem().AssignableTo(fieldType) { - // if field's type is struct, then need to set value back to argument after save - setFieldBackToValue = true - field.Set(reflectValue.Elem()) - } else if fieldType.Kind() == reflect.Slice { - if reflectValue.Type().AssignableTo(fieldType.Elem()) { - field.Set(reflect.Append(field.Field, reflectValue)) - } else if reflectValue.Type().Elem().AssignableTo(fieldType.Elem()) { - // if field's type is slice of struct, then need to set value back to argument after save - setSliceFieldBackToValue = true - field.Set(reflect.Append(field.Field, reflectValue.Elem())) - } - } - - if relationship.Kind == "many_to_many" { - association.setErr(relationship.JoinTableHandler.Add(relationship.JoinTableHandler, scope.NewDB(), scope.Value, reflectValue.Interface())) - } else { - association.setErr(scope.NewDB().Select(field.Name).Save(scope.Value).Error) - - if setFieldBackToValue { - reflectValue.Elem().Set(field.Field) - } else if setSliceFieldBackToValue { - reflectValue.Elem().Set(field.Field.Index(field.Field.Len() - 1)) - } - } - } - - for _, value := range values { - reflectValue := reflect.ValueOf(value) - indirectReflectValue := reflect.Indirect(reflectValue) - if indirectReflectValue.Kind() == reflect.Struct { - saveAssociation(reflectValue) - } else if indirectReflectValue.Kind() == reflect.Slice { - for i := 0; i < indirectReflectValue.Len(); i++ { - saveAssociation(indirectReflectValue.Index(i)) - } - } else { - association.setErr(errors.New("invalid value type")) - } - } - return association -} - -func toQueryMarks(primaryValues [][]interface{}) string { - var results []string - - for _, primaryValue := range primaryValues { - var marks []string - for _ = range primaryValue { - marks = append(marks, "?") - } - - if len(marks) > 1 { - results = append(results, fmt.Sprintf("(%v)", strings.Join(marks, ","))) - } else { - results = append(results, strings.Join(marks, "")) - } - } - return strings.Join(results, ",") -} - -func toQueryCondition(scope *Scope, columns []string) string { - var newColumns []string - for _, column := range columns { - newColumns = append(newColumns, scope.Quote(column)) - } - - if len(columns) > 1 { - return fmt.Sprintf("(%v)", strings.Join(newColumns, ",")) - } - return strings.Join(newColumns, ",") -} - -func toQueryValues(primaryValues [][]interface{}) (values []interface{}) { - for _, primaryValue := range primaryValues { - for _, value := range primaryValue { - values = append(values, value) - } - } - return values -} diff --git a/main.go b/main.go index 376a967f..ce3f7fb1 100644 --- a/main.go +++ b/main.go @@ -480,7 +480,7 @@ func (s *DB) Association(column string) *Association { if field.Relationship == nil || len(field.Relationship.ForeignFieldNames) == 0 { err = fmt.Errorf("invalid association %v for %v", column, scope.IndirectValue().Type()) } else { - return &Association{Scope: scope, Column: column, Field: field} + return &Association{scope: scope, column: column, field: field} } } else { err = fmt.Errorf("%v doesn't have column %v", scope.IndirectValue().Type(), column) diff --git a/utils.go b/utils.go index a4cf0b8c..58b14ac4 100644 --- a/utils.go +++ b/utils.go @@ -2,6 +2,7 @@ package gorm import ( "bytes" + "fmt" "strings" "sync" ) @@ -100,3 +101,42 @@ type expr struct { func Expr(expression string, args ...interface{}) *expr { return &expr{expr: expression, args: args} } + +func toQueryMarks(primaryValues [][]interface{}) string { + var results []string + + for _, primaryValue := range primaryValues { + var marks []string + for _ = range primaryValue { + marks = append(marks, "?") + } + + if len(marks) > 1 { + results = append(results, fmt.Sprintf("(%v)", strings.Join(marks, ","))) + } else { + results = append(results, strings.Join(marks, "")) + } + } + return strings.Join(results, ",") +} + +func toQueryCondition(scope *Scope, columns []string) string { + var newColumns []string + for _, column := range columns { + newColumns = append(newColumns, scope.Quote(column)) + } + + if len(columns) > 1 { + return fmt.Sprintf("(%v)", strings.Join(newColumns, ",")) + } + return strings.Join(newColumns, ",") +} + +func toQueryValues(primaryValues [][]interface{}) (values []interface{}) { + for _, primaryValue := range primaryValues { + for _, value := range primaryValue { + values = append(values, value) + } + } + return values +} From f1237e4fe96bbc4cd67b63b026be18d5343e8cfc Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sat, 16 Jan 2016 19:20:52 +0800 Subject: [PATCH 18/83] Refactor Callback --- callback.go | 125 +++++++++++++++++++++++++++++---------------- callback_create.go | 18 +++---- callback_delete.go | 10 ++-- callback_query.go | 6 +-- callback_test.go | 88 +++++++++++++++---------------- callback_update.go | 18 +++---- main.go | 44 ++++++++-------- scope_private.go | 4 +- 8 files changed, 174 insertions(+), 139 deletions(-) diff --git a/callback.go b/callback.go index 603e5111..f45bb64c 100644 --- a/callback.go +++ b/callback.go @@ -4,34 +4,45 @@ import ( "fmt" ) -type callback struct { +// defaultCallbacks hold default callbacks defined by gorm +var defaultCallbacks = &Callbacks{} + +// Callbacks contains callbacks that used when CURD objects +// Field `creates` hold callbacks will be call when creating object +// Field `updates` hold callbacks will be call when updating object +// Field `deletes` hold callbacks will be call when deleting object +// Field `queries` hold callbacks will be call when querying object with query methods like Find, First, Related, Association... +// Field `rowQueries` hold callbacks will be call when querying object with Row, Rows... +// Field `processors` hold all callback processors, will be used to generate above callbacks in order +type Callbacks struct { creates []*func(scope *Scope) updates []*func(scope *Scope) deletes []*func(scope *Scope) queries []*func(scope *Scope) rowQueries []*func(scope *Scope) - processors []*callbackProcessor + processors []*CallbackProcessor } -type callbackProcessor struct { - name string - before string - after string - replace bool - remove bool - typ string - processor *func(scope *Scope) - callback *callback +// callbackProcessor contains all informations for a callback +type CallbackProcessor struct { + name string // current callback's name + before string // register current callback before a callback + after string // register current callback after a callback + replace bool // replace callbacks with same name + remove bool // delete callbacks with same name + kind string // callback type: create, update, delete, query, row_query + processor *func(scope *Scope) // callback handler + parent *Callbacks } -func (c *callback) addProcessor(typ string) *callbackProcessor { - cp := &callbackProcessor{typ: typ, callback: c} +func (c *Callbacks) addProcessor(kind string) *CallbackProcessor { + cp := &CallbackProcessor{kind: kind, parent: c} c.processors = append(c.processors, cp) return cp } -func (c *callback) clone() *callback { - return &callback{ +func (c *Callbacks) clone() *Callbacks { + return &Callbacks{ creates: c.creates, updates: c.updates, deletes: c.deletes, @@ -40,57 +51,81 @@ func (c *callback) clone() *callback { } } -func (c *callback) Create() *callbackProcessor { +// Create could be used to register callbacks for creating object +// db.Callback().Create().After("gorm:create").Register("plugin:run_after_create", func(*Scope) { +// // business logic +// ... +// +// // set error if some thing wrong happened, will rollback the creating +// scope.Err(errors.New("error")) +// }) +func (c *Callbacks) Create() *CallbackProcessor { return c.addProcessor("create") } -func (c *callback) Update() *callbackProcessor { +// Update could be used to register callbacks for updating object, refer `Create` for usage +func (c *Callbacks) Update() *CallbackProcessor { return c.addProcessor("update") } -func (c *callback) Delete() *callbackProcessor { +// Delete could be used to register callbacks for deleting object, refer `Create` for usage +func (c *Callbacks) Delete() *CallbackProcessor { return c.addProcessor("delete") } -func (c *callback) Query() *callbackProcessor { +// Query could be used to register callbacks for querying objects with query methods like `Find`, `First`, `Related`, `Association`... +// refer `Create` for usage +func (c *Callbacks) Query() *CallbackProcessor { return c.addProcessor("query") } -func (c *callback) RowQuery() *callbackProcessor { +// Query could be used to register callbacks for querying objects with `Row`, `Rows`, refer `Create` for usage +func (c *Callbacks) RowQuery() *CallbackProcessor { return c.addProcessor("row_query") } -func (cp *callbackProcessor) Before(name string) *callbackProcessor { - cp.before = name +// After insert a new callback after callback `callbackName`, refer `Callbacks.Create` +func (cp *CallbackProcessor) After(callbackName string) *CallbackProcessor { + cp.after = callbackName return cp } -func (cp *callbackProcessor) After(name string) *callbackProcessor { - cp.after = name +// Before insert a new callback before callback `callbackName`, refer `Callbacks.Create` +func (cp *CallbackProcessor) Before(callbackName string) *CallbackProcessor { + cp.before = callbackName return cp } -func (cp *callbackProcessor) Register(name string, fc func(scope *Scope)) { - cp.name = name - cp.processor = &fc - cp.callback.sort() +// Register a new callback, refer `Callbacks.Create` +func (cp *CallbackProcessor) Register(callbackName string, callback func(scope *Scope)) { + cp.name = callbackName + cp.processor = &callback + cp.parent.reorder() } -func (cp *callbackProcessor) Remove(name string) { - fmt.Printf("[info] removing callback `%v` from %v\n", name, fileWithLineNum()) - cp.name = name +// Remove a registered callback +// db.Callback().Create().Remove("gorm:update_time_stamp_when_create") +func (cp *CallbackProcessor) Remove(callbackName string) { + fmt.Printf("[info] removing callback `%v` from %v\n", callbackName, fileWithLineNum()) + cp.name = callbackName cp.remove = true - cp.callback.sort() + cp.parent.reorder() } -func (cp *callbackProcessor) Replace(name string, fc func(scope *Scope)) { - fmt.Printf("[info] replacing callback `%v` from %v\n", name, fileWithLineNum()) - cp.name = name - cp.processor = &fc +// Replace a registered callback with new callback +// db.Callback().Create().Replace("gorm:update_time_stamp_when_create", func(*Scope) { +// scope.SetColumn("Created", now) +// scope.SetColumn("Updated", now) +// }) +func (cp *CallbackProcessor) Replace(callbackName string, callback func(scope *Scope)) { + fmt.Printf("[info] replacing callback `%v` from %v\n", callbackName, fileWithLineNum()) + cp.name = callbackName + cp.processor = &callback cp.replace = true - cp.callback.sort() + cp.parent.reorder() } +// getRIndex get right index from string slice func getRIndex(strs []string, str string) int { for i := len(strs) - 1; i >= 0; i-- { if strs[i] == str { @@ -100,8 +135,9 @@ func getRIndex(strs []string, str string) int { return -1 } -func sortProcessors(cps []*callbackProcessor) []*func(scope *Scope) { - var sortCallbackProcessor func(c *callbackProcessor) +// sortProcessors sort callback processors based on its before, after, remove, replace +func sortProcessors(cps []*CallbackProcessor) []*func(scope *Scope) { + var sortCallbackProcessor func(c *CallbackProcessor) var names, sortedNames = []string{}, []string{} for _, cp := range cps { @@ -113,7 +149,7 @@ func sortProcessors(cps []*callbackProcessor) []*func(scope *Scope) { names = append(names, cp.name) } - sortCallbackProcessor = func(c *callbackProcessor) { + sortCallbackProcessor = func(c *CallbackProcessor) { if getRIndex(sortedNames, c.name) > -1 { return } @@ -172,11 +208,12 @@ func sortProcessors(cps []*callbackProcessor) []*func(scope *Scope) { return append(sortedFuncs, funcs...) } -func (c *callback) sort() { - var creates, updates, deletes, queries, rowQueries []*callbackProcessor +// reorder all registered processors, and reset CURD callbacks +func (c *Callbacks) reorder() { + var creates, updates, deletes, queries, rowQueries []*CallbackProcessor for _, processor := range c.processors { - switch processor.typ { + switch processor.kind { case "create": creates = append(creates, processor) case "update": @@ -196,5 +233,3 @@ func (c *callback) sort() { c.queries = sortProcessors(queries) c.rowQueries = sortProcessors(rowQueries) } - -var DefaultCallback = &callback{processors: []*callbackProcessor{}} diff --git a/callback_create.go b/callback_create.go index 07121544..6f99c56b 100644 --- a/callback_create.go +++ b/callback_create.go @@ -114,13 +114,13 @@ func AfterCreate(scope *Scope) { } func init() { - DefaultCallback.Create().Register("gorm:begin_transaction", BeginTransaction) - DefaultCallback.Create().Register("gorm:before_create", BeforeCreate) - DefaultCallback.Create().Register("gorm:save_before_associations", SaveBeforeAssociations) - DefaultCallback.Create().Register("gorm:update_time_stamp_when_create", UpdateTimeStampWhenCreate) - DefaultCallback.Create().Register("gorm:create", Create) - DefaultCallback.Create().Register("gorm:force_reload_after_create", ForceReloadAfterCreate) - DefaultCallback.Create().Register("gorm:save_after_associations", SaveAfterAssociations) - DefaultCallback.Create().Register("gorm:after_create", AfterCreate) - DefaultCallback.Create().Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) + defaultCallbacks.Create().Register("gorm:begin_transaction", BeginTransaction) + defaultCallbacks.Create().Register("gorm:before_create", BeforeCreate) + defaultCallbacks.Create().Register("gorm:save_before_associations", SaveBeforeAssociations) + defaultCallbacks.Create().Register("gorm:update_time_stamp_when_create", UpdateTimeStampWhenCreate) + defaultCallbacks.Create().Register("gorm:create", Create) + defaultCallbacks.Create().Register("gorm:force_reload_after_create", ForceReloadAfterCreate) + defaultCallbacks.Create().Register("gorm:save_after_associations", SaveAfterAssociations) + defaultCallbacks.Create().Register("gorm:after_create", AfterCreate) + defaultCallbacks.Create().Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) } diff --git a/callback_delete.go b/callback_delete.go index 72236659..7ea001cc 100644 --- a/callback_delete.go +++ b/callback_delete.go @@ -28,9 +28,9 @@ func AfterDelete(scope *Scope) { } func init() { - DefaultCallback.Delete().Register("gorm:begin_transaction", BeginTransaction) - DefaultCallback.Delete().Register("gorm:before_delete", BeforeDelete) - DefaultCallback.Delete().Register("gorm:delete", Delete) - DefaultCallback.Delete().Register("gorm:after_delete", AfterDelete) - DefaultCallback.Delete().Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) + defaultCallbacks.Delete().Register("gorm:begin_transaction", BeginTransaction) + defaultCallbacks.Delete().Register("gorm:before_delete", BeforeDelete) + defaultCallbacks.Delete().Register("gorm:delete", Delete) + defaultCallbacks.Delete().Register("gorm:after_delete", AfterDelete) + defaultCallbacks.Delete().Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) } diff --git a/callback_query.go b/callback_query.go index f837d069..2c9ba0d1 100644 --- a/callback_query.go +++ b/callback_query.go @@ -83,7 +83,7 @@ func AfterQuery(scope *Scope) { } func init() { - DefaultCallback.Query().Register("gorm:query", Query) - DefaultCallback.Query().Register("gorm:after_query", AfterQuery) - DefaultCallback.Query().Register("gorm:preload", Preload) + defaultCallbacks.Query().Register("gorm:query", Query) + defaultCallbacks.Query().Register("gorm:after_query", AfterQuery) + defaultCallbacks.Query().Register("gorm:preload", Preload) } diff --git a/callback_test.go b/callback_test.go index b416d6af..bb189543 100644 --- a/callback_test.go +++ b/callback_test.go @@ -23,62 +23,62 @@ func afterCreate1(s *Scope) {} func afterCreate2(s *Scope) {} func TestRegisterCallback(t *testing.T) { - var callback = &callback{processors: []*callbackProcessor{}} + var callbacks = &Callbacks{} - callback.Create().Register("before_create1", beforeCreate1) - callback.Create().Register("before_create2", beforeCreate2) - callback.Create().Register("create", create) - callback.Create().Register("after_create1", afterCreate1) - callback.Create().Register("after_create2", afterCreate2) + callbacks.Create().Register("before_create1", beforeCreate1) + callbacks.Create().Register("before_create2", beforeCreate2) + callbacks.Create().Register("create", create) + callbacks.Create().Register("after_create1", afterCreate1) + callbacks.Create().Register("after_create2", afterCreate2) - if !equalFuncs(callback.creates, []string{"beforeCreate1", "beforeCreate2", "create", "afterCreate1", "afterCreate2"}) { + if !equalFuncs(callbacks.creates, []string{"beforeCreate1", "beforeCreate2", "create", "afterCreate1", "afterCreate2"}) { t.Errorf("register callback") } } func TestRegisterCallbackWithOrder(t *testing.T) { - var callback1 = &callback{processors: []*callbackProcessor{}} - callback1.Create().Register("before_create1", beforeCreate1) - callback1.Create().Register("create", create) - callback1.Create().Register("after_create1", afterCreate1) - callback1.Create().Before("after_create1").Register("after_create2", afterCreate2) - if !equalFuncs(callback1.creates, []string{"beforeCreate1", "create", "afterCreate2", "afterCreate1"}) { + var callbacks1 = &Callbacks{} + callbacks1.Create().Register("before_create1", beforeCreate1) + callbacks1.Create().Register("create", create) + callbacks1.Create().Register("after_create1", afterCreate1) + callbacks1.Create().Before("after_create1").Register("after_create2", afterCreate2) + if !equalFuncs(callbacks1.creates, []string{"beforeCreate1", "create", "afterCreate2", "afterCreate1"}) { t.Errorf("register callback with order") } - var callback2 = &callback{processors: []*callbackProcessor{}} + var callbacks2 = &Callbacks{} - callback2.Update().Register("create", create) - callback2.Update().Before("create").Register("before_create1", beforeCreate1) - callback2.Update().After("after_create2").Register("after_create1", afterCreate1) - callback2.Update().Before("before_create1").Register("before_create2", beforeCreate2) - callback2.Update().Register("after_create2", afterCreate2) + callbacks2.Update().Register("create", create) + callbacks2.Update().Before("create").Register("before_create1", beforeCreate1) + callbacks2.Update().After("after_create2").Register("after_create1", afterCreate1) + callbacks2.Update().Before("before_create1").Register("before_create2", beforeCreate2) + callbacks2.Update().Register("after_create2", afterCreate2) - if !equalFuncs(callback2.updates, []string{"beforeCreate2", "beforeCreate1", "create", "afterCreate2", "afterCreate1"}) { + if !equalFuncs(callbacks2.updates, []string{"beforeCreate2", "beforeCreate1", "create", "afterCreate2", "afterCreate1"}) { t.Errorf("register callback with order") } } func TestRegisterCallbackWithComplexOrder(t *testing.T) { - var callback1 = &callback{processors: []*callbackProcessor{}} + var callbacks1 = &Callbacks{} - callback1.Query().Before("after_create1").After("before_create1").Register("create", create) - callback1.Query().Register("before_create1", beforeCreate1) - callback1.Query().Register("after_create1", afterCreate1) + callbacks1.Query().Before("after_create1").After("before_create1").Register("create", create) + callbacks1.Query().Register("before_create1", beforeCreate1) + callbacks1.Query().Register("after_create1", afterCreate1) - if !equalFuncs(callback1.queries, []string{"beforeCreate1", "create", "afterCreate1"}) { + if !equalFuncs(callbacks1.queries, []string{"beforeCreate1", "create", "afterCreate1"}) { t.Errorf("register callback with order") } - var callback2 = &callback{processors: []*callbackProcessor{}} + var callbacks2 = &Callbacks{} - callback2.Delete().Before("after_create1").After("before_create1").Register("create", create) - callback2.Delete().Before("create").Register("before_create1", beforeCreate1) - callback2.Delete().After("before_create1").Register("before_create2", beforeCreate2) - callback2.Delete().Register("after_create1", afterCreate1) - callback2.Delete().After("after_create1").Register("after_create2", afterCreate2) + callbacks2.Delete().Before("after_create1").After("before_create1").Register("create", create) + callbacks2.Delete().Before("create").Register("before_create1", beforeCreate1) + callbacks2.Delete().After("before_create1").Register("before_create2", beforeCreate2) + callbacks2.Delete().Register("after_create1", afterCreate1) + callbacks2.Delete().After("after_create1").Register("after_create2", afterCreate2) - if !equalFuncs(callback2.deletes, []string{"beforeCreate1", "beforeCreate2", "create", "afterCreate1", "afterCreate2"}) { + if !equalFuncs(callbacks2.deletes, []string{"beforeCreate1", "beforeCreate2", "create", "afterCreate1", "afterCreate2"}) { t.Errorf("register callback with order") } } @@ -86,27 +86,27 @@ func TestRegisterCallbackWithComplexOrder(t *testing.T) { func replaceCreate(s *Scope) {} func TestReplaceCallback(t *testing.T) { - var callback = &callback{processors: []*callbackProcessor{}} + var callbacks = &Callbacks{} - callback.Create().Before("after_create1").After("before_create1").Register("create", create) - callback.Create().Register("before_create1", beforeCreate1) - callback.Create().Register("after_create1", afterCreate1) - callback.Create().Replace("create", replaceCreate) + callbacks.Create().Before("after_create1").After("before_create1").Register("create", create) + callbacks.Create().Register("before_create1", beforeCreate1) + callbacks.Create().Register("after_create1", afterCreate1) + callbacks.Create().Replace("create", replaceCreate) - if !equalFuncs(callback.creates, []string{"beforeCreate1", "replaceCreate", "afterCreate1"}) { + if !equalFuncs(callbacks.creates, []string{"beforeCreate1", "replaceCreate", "afterCreate1"}) { t.Errorf("replace callback") } } func TestRemoveCallback(t *testing.T) { - var callback = &callback{processors: []*callbackProcessor{}} + var callbacks = &Callbacks{} - callback.Create().Before("after_create1").After("before_create1").Register("create", create) - callback.Create().Register("before_create1", beforeCreate1) - callback.Create().Register("after_create1", afterCreate1) - callback.Create().Remove("create") + callbacks.Create().Before("after_create1").After("before_create1").Register("create", create) + callbacks.Create().Register("before_create1", beforeCreate1) + callbacks.Create().Register("after_create1", afterCreate1) + callbacks.Create().Remove("create") - if !equalFuncs(callback.creates, []string{"beforeCreate1", "afterCreate1"}) { + if !equalFuncs(callbacks.creates, []string{"beforeCreate1", "afterCreate1"}) { t.Errorf("remove callback") } } diff --git a/callback_update.go b/callback_update.go index 4c9952d2..a2b6d48e 100644 --- a/callback_update.go +++ b/callback_update.go @@ -83,13 +83,13 @@ func AfterUpdate(scope *Scope) { } func init() { - DefaultCallback.Update().Register("gorm:assign_update_attributes", AssignUpdateAttributes) - DefaultCallback.Update().Register("gorm:begin_transaction", BeginTransaction) - DefaultCallback.Update().Register("gorm:before_update", BeforeUpdate) - DefaultCallback.Update().Register("gorm:save_before_associations", SaveBeforeAssociations) - DefaultCallback.Update().Register("gorm:update_time_stamp_when_update", UpdateTimeStampWhenUpdate) - DefaultCallback.Update().Register("gorm:update", Update) - DefaultCallback.Update().Register("gorm:save_after_associations", SaveAfterAssociations) - DefaultCallback.Update().Register("gorm:after_update", AfterUpdate) - DefaultCallback.Update().Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) + defaultCallbacks.Update().Register("gorm:assign_update_attributes", AssignUpdateAttributes) + defaultCallbacks.Update().Register("gorm:begin_transaction", BeginTransaction) + defaultCallbacks.Update().Register("gorm:before_update", BeforeUpdate) + defaultCallbacks.Update().Register("gorm:save_before_associations", SaveBeforeAssociations) + defaultCallbacks.Update().Register("gorm:update_time_stamp_when_update", UpdateTimeStampWhenUpdate) + defaultCallbacks.Update().Register("gorm:update", Update) + defaultCallbacks.Update().Register("gorm:save_after_associations", SaveAfterAssociations) + defaultCallbacks.Update().Register("gorm:after_update", AfterUpdate) + defaultCallbacks.Update().Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) } diff --git a/main.go b/main.go index ce3f7fb1..bc5f4735 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ type DB struct { Value interface{} Error error RowsAffected int64 - callback *callback + callbacks *Callbacks db sqlCommon parent *DB search *search @@ -65,12 +65,12 @@ func Open(dialect string, args ...interface{}) (*DB, error) { } db = DB{ - dialect: NewDialect(dialect), - logger: defaultLogger, - callback: DefaultCallback, - source: source, - values: map[string]interface{}{}, - db: dbSql, + dialect: NewDialect(dialect), + logger: defaultLogger, + callbacks: defaultCallbacks, + source: source, + values: map[string]interface{}{}, + db: dbSql, } db.parent = &db @@ -111,9 +111,9 @@ func (s *DB) CommonDB() sqlCommon { return s.db } -func (s *DB) Callback() *callback { - s.parent.callback = s.parent.callback.clone() - return s.parent.callback +func (s *DB) Callback() *Callbacks { + s.parent.callbacks = s.parent.callbacks.clone() + return s.parent.callbacks } func (s *DB) SetLogger(l logger) { @@ -201,22 +201,22 @@ func (s *DB) First(out interface{}, where ...interface{}) *DB { newScope := s.clone().NewScope(out) newScope.Search.Limit(1) return newScope.Set("gorm:order_by_primary_key", "ASC"). - inlineCondition(where...).callCallbacks(s.parent.callback.queries).db + inlineCondition(where...).callCallbacks(s.parent.callbacks.queries).db } func (s *DB) Last(out interface{}, where ...interface{}) *DB { newScope := s.clone().NewScope(out) newScope.Search.Limit(1) return newScope.Set("gorm:order_by_primary_key", "DESC"). - inlineCondition(where...).callCallbacks(s.parent.callback.queries).db + inlineCondition(where...).callCallbacks(s.parent.callbacks.queries).db } func (s *DB) Find(out interface{}, where ...interface{}) *DB { - return s.clone().NewScope(out).inlineCondition(where...).callCallbacks(s.parent.callback.queries).db + return s.clone().NewScope(out).inlineCondition(where...).callCallbacks(s.parent.callbacks.queries).db } func (s *DB) Scan(dest interface{}) *DB { - return s.clone().NewScope(s.Value).Set("gorm:query_destination", dest).callCallbacks(s.parent.callback.queries).db + return s.clone().NewScope(s.Value).Set("gorm:query_destination", dest).callCallbacks(s.parent.callbacks.queries).db } func (s *DB) Row() *sql.Row { @@ -258,9 +258,9 @@ func (s *DB) FirstOrCreate(out interface{}, where ...interface{}) *DB { if !result.RecordNotFound() { return result } - c.AddError(c.NewScope(out).inlineCondition(where...).initialize().callCallbacks(c.parent.callback.creates).db.Error) + c.AddError(c.NewScope(out).inlineCondition(where...).initialize().callCallbacks(c.parent.callbacks.creates).db.Error) } else if len(c.search.assignAttrs) > 0 { - c.AddError(c.NewScope(out).InstanceSet("gorm:update_interface", c.search.assignAttrs).callCallbacks(c.parent.callback.updates).db.Error) + c.AddError(c.NewScope(out).InstanceSet("gorm:update_interface", c.search.assignAttrs).callCallbacks(c.parent.callbacks.updates).db.Error) } return c } @@ -273,7 +273,7 @@ func (s *DB) Updates(values interface{}, ignoreProtectedAttrs ...bool) *DB { return s.clone().NewScope(s.Value). Set("gorm:ignore_protected_attrs", len(ignoreProtectedAttrs) > 0). InstanceSet("gorm:update_interface", values). - callCallbacks(s.parent.callback.updates).db + callCallbacks(s.parent.callbacks.updates).db } func (s *DB) UpdateColumn(attrs ...interface{}) *DB { @@ -285,24 +285,24 @@ func (s *DB) UpdateColumns(values interface{}) *DB { Set("gorm:update_column", true). Set("gorm:save_associations", false). InstanceSet("gorm:update_interface", values). - callCallbacks(s.parent.callback.updates).db + callCallbacks(s.parent.callbacks.updates).db } func (s *DB) Save(value interface{}) *DB { scope := s.clone().NewScope(value) if scope.PrimaryKeyZero() { - return scope.callCallbacks(s.parent.callback.creates).db + return scope.callCallbacks(s.parent.callbacks.creates).db } - return scope.callCallbacks(s.parent.callback.updates).db + return scope.callCallbacks(s.parent.callbacks.updates).db } func (s *DB) Create(value interface{}) *DB { scope := s.clone().NewScope(value) - return scope.callCallbacks(s.parent.callback.creates).db + return scope.callCallbacks(s.parent.callbacks.creates).db } func (s *DB) Delete(value interface{}, where ...interface{}) *DB { - return s.clone().NewScope(value).inlineCondition(where...).callCallbacks(s.parent.callback.deletes).db + return s.clone().NewScope(value).inlineCondition(where...).callCallbacks(s.parent.callbacks.deletes).db } func (s *DB) Raw(sql string, values ...interface{}) *DB { diff --git a/scope_private.go b/scope_private.go index 9d0283d7..769d4c64 100644 --- a/scope_private.go +++ b/scope_private.go @@ -377,14 +377,14 @@ func (scope *Scope) updatedAttrsWithValues(values map[string]interface{}, ignore func (scope *Scope) row() *sql.Row { defer scope.trace(NowFunc()) - scope.callCallbacks(scope.db.parent.callback.rowQueries) + scope.callCallbacks(scope.db.parent.callbacks.rowQueries) scope.prepareQuerySql() return scope.SqlDB().QueryRow(scope.Sql, scope.SqlVars...) } func (scope *Scope) rows() (*sql.Rows, error) { defer scope.trace(NowFunc()) - scope.callCallbacks(scope.db.parent.callback.rowQueries) + scope.callCallbacks(scope.db.parent.callbacks.rowQueries) scope.prepareQuerySql() return scope.SqlDB().Query(scope.Sql, scope.SqlVars...) } From 8e2aaa92c9e642481e0807a3aebd70a5ee23250e Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sat, 16 Jan 2016 21:55:00 +0800 Subject: [PATCH 19/83] Rename Callbacks to Callback --- callback.go | 28 +++++++-------- callback_create.go | 18 +++++----- callback_delete.go | 10 +++--- callback_query.go | 6 ++-- callback_test.go | 88 +++++++++++++++++++++++----------------------- callback_update.go | 18 +++++----- main.go | 6 ++-- 7 files changed, 87 insertions(+), 87 deletions(-) diff --git a/callback.go b/callback.go index f45bb64c..55cd807e 100644 --- a/callback.go +++ b/callback.go @@ -4,17 +4,17 @@ import ( "fmt" ) -// defaultCallbacks hold default callbacks defined by gorm -var defaultCallbacks = &Callbacks{} +// defaultCallback hold default callbacks defined by gorm +var defaultCallback = &Callback{} -// Callbacks contains callbacks that used when CURD objects +// Callback contains callbacks that used when CURD objects // Field `creates` hold callbacks will be call when creating object // Field `updates` hold callbacks will be call when updating object // Field `deletes` hold callbacks will be call when deleting object // Field `queries` hold callbacks will be call when querying object with query methods like Find, First, Related, Association... // Field `rowQueries` hold callbacks will be call when querying object with Row, Rows... // Field `processors` hold all callback processors, will be used to generate above callbacks in order -type Callbacks struct { +type Callback struct { creates []*func(scope *Scope) updates []*func(scope *Scope) deletes []*func(scope *Scope) @@ -32,17 +32,17 @@ type CallbackProcessor struct { remove bool // delete callbacks with same name kind string // callback type: create, update, delete, query, row_query processor *func(scope *Scope) // callback handler - parent *Callbacks + parent *Callback } -func (c *Callbacks) addProcessor(kind string) *CallbackProcessor { +func (c *Callback) addProcessor(kind string) *CallbackProcessor { cp := &CallbackProcessor{kind: kind, parent: c} c.processors = append(c.processors, cp) return cp } -func (c *Callbacks) clone() *Callbacks { - return &Callbacks{ +func (c *Callback) clone() *Callback { + return &Callback{ creates: c.creates, updates: c.updates, deletes: c.deletes, @@ -59,28 +59,28 @@ func (c *Callbacks) clone() *Callbacks { // // set error if some thing wrong happened, will rollback the creating // scope.Err(errors.New("error")) // }) -func (c *Callbacks) Create() *CallbackProcessor { +func (c *Callback) Create() *CallbackProcessor { return c.addProcessor("create") } // Update could be used to register callbacks for updating object, refer `Create` for usage -func (c *Callbacks) Update() *CallbackProcessor { +func (c *Callback) Update() *CallbackProcessor { return c.addProcessor("update") } // Delete could be used to register callbacks for deleting object, refer `Create` for usage -func (c *Callbacks) Delete() *CallbackProcessor { +func (c *Callback) Delete() *CallbackProcessor { return c.addProcessor("delete") } // Query could be used to register callbacks for querying objects with query methods like `Find`, `First`, `Related`, `Association`... // refer `Create` for usage -func (c *Callbacks) Query() *CallbackProcessor { +func (c *Callback) Query() *CallbackProcessor { return c.addProcessor("query") } // Query could be used to register callbacks for querying objects with `Row`, `Rows`, refer `Create` for usage -func (c *Callbacks) RowQuery() *CallbackProcessor { +func (c *Callback) RowQuery() *CallbackProcessor { return c.addProcessor("row_query") } @@ -209,7 +209,7 @@ func sortProcessors(cps []*CallbackProcessor) []*func(scope *Scope) { } // reorder all registered processors, and reset CURD callbacks -func (c *Callbacks) reorder() { +func (c *Callback) reorder() { var creates, updates, deletes, queries, rowQueries []*CallbackProcessor for _, processor := range c.processors { diff --git a/callback_create.go b/callback_create.go index 6f99c56b..c52b9c85 100644 --- a/callback_create.go +++ b/callback_create.go @@ -114,13 +114,13 @@ func AfterCreate(scope *Scope) { } func init() { - defaultCallbacks.Create().Register("gorm:begin_transaction", BeginTransaction) - defaultCallbacks.Create().Register("gorm:before_create", BeforeCreate) - defaultCallbacks.Create().Register("gorm:save_before_associations", SaveBeforeAssociations) - defaultCallbacks.Create().Register("gorm:update_time_stamp_when_create", UpdateTimeStampWhenCreate) - defaultCallbacks.Create().Register("gorm:create", Create) - defaultCallbacks.Create().Register("gorm:force_reload_after_create", ForceReloadAfterCreate) - defaultCallbacks.Create().Register("gorm:save_after_associations", SaveAfterAssociations) - defaultCallbacks.Create().Register("gorm:after_create", AfterCreate) - defaultCallbacks.Create().Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) + defaultCallback.Create().Register("gorm:begin_transaction", BeginTransaction) + defaultCallback.Create().Register("gorm:before_create", BeforeCreate) + defaultCallback.Create().Register("gorm:save_before_associations", SaveBeforeAssociations) + defaultCallback.Create().Register("gorm:update_time_stamp_when_create", UpdateTimeStampWhenCreate) + defaultCallback.Create().Register("gorm:create", Create) + defaultCallback.Create().Register("gorm:force_reload_after_create", ForceReloadAfterCreate) + defaultCallback.Create().Register("gorm:save_after_associations", SaveAfterAssociations) + defaultCallback.Create().Register("gorm:after_create", AfterCreate) + defaultCallback.Create().Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) } diff --git a/callback_delete.go b/callback_delete.go index 7ea001cc..dca6ee21 100644 --- a/callback_delete.go +++ b/callback_delete.go @@ -28,9 +28,9 @@ func AfterDelete(scope *Scope) { } func init() { - defaultCallbacks.Delete().Register("gorm:begin_transaction", BeginTransaction) - defaultCallbacks.Delete().Register("gorm:before_delete", BeforeDelete) - defaultCallbacks.Delete().Register("gorm:delete", Delete) - defaultCallbacks.Delete().Register("gorm:after_delete", AfterDelete) - defaultCallbacks.Delete().Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) + defaultCallback.Delete().Register("gorm:begin_transaction", BeginTransaction) + defaultCallback.Delete().Register("gorm:before_delete", BeforeDelete) + defaultCallback.Delete().Register("gorm:delete", Delete) + defaultCallback.Delete().Register("gorm:after_delete", AfterDelete) + defaultCallback.Delete().Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) } diff --git a/callback_query.go b/callback_query.go index 2c9ba0d1..05ac8880 100644 --- a/callback_query.go +++ b/callback_query.go @@ -83,7 +83,7 @@ func AfterQuery(scope *Scope) { } func init() { - defaultCallbacks.Query().Register("gorm:query", Query) - defaultCallbacks.Query().Register("gorm:after_query", AfterQuery) - defaultCallbacks.Query().Register("gorm:preload", Preload) + defaultCallback.Query().Register("gorm:query", Query) + defaultCallback.Query().Register("gorm:after_query", AfterQuery) + defaultCallback.Query().Register("gorm:preload", Preload) } diff --git a/callback_test.go b/callback_test.go index bb189543..13ca3f42 100644 --- a/callback_test.go +++ b/callback_test.go @@ -23,62 +23,62 @@ func afterCreate1(s *Scope) {} func afterCreate2(s *Scope) {} func TestRegisterCallback(t *testing.T) { - var callbacks = &Callbacks{} + var callback = &Callback{} - callbacks.Create().Register("before_create1", beforeCreate1) - callbacks.Create().Register("before_create2", beforeCreate2) - callbacks.Create().Register("create", create) - callbacks.Create().Register("after_create1", afterCreate1) - callbacks.Create().Register("after_create2", afterCreate2) + callback.Create().Register("before_create1", beforeCreate1) + callback.Create().Register("before_create2", beforeCreate2) + callback.Create().Register("create", create) + callback.Create().Register("after_create1", afterCreate1) + callback.Create().Register("after_create2", afterCreate2) - if !equalFuncs(callbacks.creates, []string{"beforeCreate1", "beforeCreate2", "create", "afterCreate1", "afterCreate2"}) { + if !equalFuncs(callback.creates, []string{"beforeCreate1", "beforeCreate2", "create", "afterCreate1", "afterCreate2"}) { t.Errorf("register callback") } } func TestRegisterCallbackWithOrder(t *testing.T) { - var callbacks1 = &Callbacks{} - callbacks1.Create().Register("before_create1", beforeCreate1) - callbacks1.Create().Register("create", create) - callbacks1.Create().Register("after_create1", afterCreate1) - callbacks1.Create().Before("after_create1").Register("after_create2", afterCreate2) - if !equalFuncs(callbacks1.creates, []string{"beforeCreate1", "create", "afterCreate2", "afterCreate1"}) { + var callback1 = &Callback{} + callback1.Create().Register("before_create1", beforeCreate1) + callback1.Create().Register("create", create) + callback1.Create().Register("after_create1", afterCreate1) + callback1.Create().Before("after_create1").Register("after_create2", afterCreate2) + if !equalFuncs(callback1.creates, []string{"beforeCreate1", "create", "afterCreate2", "afterCreate1"}) { t.Errorf("register callback with order") } - var callbacks2 = &Callbacks{} + var callback2 = &Callback{} - callbacks2.Update().Register("create", create) - callbacks2.Update().Before("create").Register("before_create1", beforeCreate1) - callbacks2.Update().After("after_create2").Register("after_create1", afterCreate1) - callbacks2.Update().Before("before_create1").Register("before_create2", beforeCreate2) - callbacks2.Update().Register("after_create2", afterCreate2) + callback2.Update().Register("create", create) + callback2.Update().Before("create").Register("before_create1", beforeCreate1) + callback2.Update().After("after_create2").Register("after_create1", afterCreate1) + callback2.Update().Before("before_create1").Register("before_create2", beforeCreate2) + callback2.Update().Register("after_create2", afterCreate2) - if !equalFuncs(callbacks2.updates, []string{"beforeCreate2", "beforeCreate1", "create", "afterCreate2", "afterCreate1"}) { + if !equalFuncs(callback2.updates, []string{"beforeCreate2", "beforeCreate1", "create", "afterCreate2", "afterCreate1"}) { t.Errorf("register callback with order") } } func TestRegisterCallbackWithComplexOrder(t *testing.T) { - var callbacks1 = &Callbacks{} + var callback1 = &Callback{} - callbacks1.Query().Before("after_create1").After("before_create1").Register("create", create) - callbacks1.Query().Register("before_create1", beforeCreate1) - callbacks1.Query().Register("after_create1", afterCreate1) + callback1.Query().Before("after_create1").After("before_create1").Register("create", create) + callback1.Query().Register("before_create1", beforeCreate1) + callback1.Query().Register("after_create1", afterCreate1) - if !equalFuncs(callbacks1.queries, []string{"beforeCreate1", "create", "afterCreate1"}) { + if !equalFuncs(callback1.queries, []string{"beforeCreate1", "create", "afterCreate1"}) { t.Errorf("register callback with order") } - var callbacks2 = &Callbacks{} + var callback2 = &Callback{} - callbacks2.Delete().Before("after_create1").After("before_create1").Register("create", create) - callbacks2.Delete().Before("create").Register("before_create1", beforeCreate1) - callbacks2.Delete().After("before_create1").Register("before_create2", beforeCreate2) - callbacks2.Delete().Register("after_create1", afterCreate1) - callbacks2.Delete().After("after_create1").Register("after_create2", afterCreate2) + callback2.Delete().Before("after_create1").After("before_create1").Register("create", create) + callback2.Delete().Before("create").Register("before_create1", beforeCreate1) + callback2.Delete().After("before_create1").Register("before_create2", beforeCreate2) + callback2.Delete().Register("after_create1", afterCreate1) + callback2.Delete().After("after_create1").Register("after_create2", afterCreate2) - if !equalFuncs(callbacks2.deletes, []string{"beforeCreate1", "beforeCreate2", "create", "afterCreate1", "afterCreate2"}) { + if !equalFuncs(callback2.deletes, []string{"beforeCreate1", "beforeCreate2", "create", "afterCreate1", "afterCreate2"}) { t.Errorf("register callback with order") } } @@ -86,27 +86,27 @@ func TestRegisterCallbackWithComplexOrder(t *testing.T) { func replaceCreate(s *Scope) {} func TestReplaceCallback(t *testing.T) { - var callbacks = &Callbacks{} + var callback = &Callback{} - callbacks.Create().Before("after_create1").After("before_create1").Register("create", create) - callbacks.Create().Register("before_create1", beforeCreate1) - callbacks.Create().Register("after_create1", afterCreate1) - callbacks.Create().Replace("create", replaceCreate) + callback.Create().Before("after_create1").After("before_create1").Register("create", create) + callback.Create().Register("before_create1", beforeCreate1) + callback.Create().Register("after_create1", afterCreate1) + callback.Create().Replace("create", replaceCreate) - if !equalFuncs(callbacks.creates, []string{"beforeCreate1", "replaceCreate", "afterCreate1"}) { + if !equalFuncs(callback.creates, []string{"beforeCreate1", "replaceCreate", "afterCreate1"}) { t.Errorf("replace callback") } } func TestRemoveCallback(t *testing.T) { - var callbacks = &Callbacks{} + var callback = &Callback{} - callbacks.Create().Before("after_create1").After("before_create1").Register("create", create) - callbacks.Create().Register("before_create1", beforeCreate1) - callbacks.Create().Register("after_create1", afterCreate1) - callbacks.Create().Remove("create") + callback.Create().Before("after_create1").After("before_create1").Register("create", create) + callback.Create().Register("before_create1", beforeCreate1) + callback.Create().Register("after_create1", afterCreate1) + callback.Create().Remove("create") - if !equalFuncs(callbacks.creates, []string{"beforeCreate1", "afterCreate1"}) { + if !equalFuncs(callback.creates, []string{"beforeCreate1", "afterCreate1"}) { t.Errorf("remove callback") } } diff --git a/callback_update.go b/callback_update.go index a2b6d48e..e7884450 100644 --- a/callback_update.go +++ b/callback_update.go @@ -83,13 +83,13 @@ func AfterUpdate(scope *Scope) { } func init() { - defaultCallbacks.Update().Register("gorm:assign_update_attributes", AssignUpdateAttributes) - defaultCallbacks.Update().Register("gorm:begin_transaction", BeginTransaction) - defaultCallbacks.Update().Register("gorm:before_update", BeforeUpdate) - defaultCallbacks.Update().Register("gorm:save_before_associations", SaveBeforeAssociations) - defaultCallbacks.Update().Register("gorm:update_time_stamp_when_update", UpdateTimeStampWhenUpdate) - defaultCallbacks.Update().Register("gorm:update", Update) - defaultCallbacks.Update().Register("gorm:save_after_associations", SaveAfterAssociations) - defaultCallbacks.Update().Register("gorm:after_update", AfterUpdate) - defaultCallbacks.Update().Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) + defaultCallback.Update().Register("gorm:assign_update_attributes", AssignUpdateAttributes) + defaultCallback.Update().Register("gorm:begin_transaction", BeginTransaction) + defaultCallback.Update().Register("gorm:before_update", BeforeUpdate) + defaultCallback.Update().Register("gorm:save_before_associations", SaveBeforeAssociations) + defaultCallback.Update().Register("gorm:update_time_stamp_when_update", UpdateTimeStampWhenUpdate) + defaultCallback.Update().Register("gorm:update", Update) + defaultCallback.Update().Register("gorm:save_after_associations", SaveAfterAssociations) + defaultCallback.Update().Register("gorm:after_update", AfterUpdate) + defaultCallback.Update().Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) } diff --git a/main.go b/main.go index bc5f4735..d580c5a5 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ type DB struct { Value interface{} Error error RowsAffected int64 - callbacks *Callbacks + callbacks *Callback db sqlCommon parent *DB search *search @@ -67,7 +67,7 @@ func Open(dialect string, args ...interface{}) (*DB, error) { db = DB{ dialect: NewDialect(dialect), logger: defaultLogger, - callbacks: defaultCallbacks, + callbacks: defaultCallback, source: source, values: map[string]interface{}{}, db: dbSql, @@ -111,7 +111,7 @@ func (s *DB) CommonDB() sqlCommon { return s.db } -func (s *DB) Callback() *Callbacks { +func (s *DB) Callback() *Callback { s.parent.callbacks = s.parent.callbacks.clone() return s.parent.callbacks } From 09f46f01b90fb27eba84c75d8b13bff52857b920 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sat, 16 Jan 2016 22:01:04 +0800 Subject: [PATCH 20/83] Refactor dialect --- dialect.go | 2 - common_dialect.go => dialect_common.go | 0 mssql.go => dialect_mssql.go | 0 mysql.go => dialect_mysql.go | 0 postgres.go => dialect_postgres.go | 0 sqlite3.go => dialect_sqlite3.go | 0 foundation.go | 83 -------------------------- 7 files changed, 85 deletions(-) rename common_dialect.go => dialect_common.go (100%) rename mssql.go => dialect_mssql.go (100%) rename mysql.go => dialect_mysql.go (100%) rename postgres.go => dialect_postgres.go (100%) rename sqlite3.go => dialect_sqlite3.go (100%) delete mode 100644 foundation.go diff --git a/dialect.go b/dialect.go index 926f8a11..aa23e31b 100644 --- a/dialect.go +++ b/dialect.go @@ -25,8 +25,6 @@ func NewDialect(driver string) Dialect { switch driver { case "postgres": d = &postgres{} - case "foundation": - d = &foundation{} case "mysql": d = &mysql{} case "sqlite3": diff --git a/common_dialect.go b/dialect_common.go similarity index 100% rename from common_dialect.go rename to dialect_common.go diff --git a/mssql.go b/dialect_mssql.go similarity index 100% rename from mssql.go rename to dialect_mssql.go diff --git a/mysql.go b/dialect_mysql.go similarity index 100% rename from mysql.go rename to dialect_mysql.go diff --git a/postgres.go b/dialect_postgres.go similarity index 100% rename from postgres.go rename to dialect_postgres.go diff --git a/sqlite3.go b/dialect_sqlite3.go similarity index 100% rename from sqlite3.go rename to dialect_sqlite3.go diff --git a/foundation.go b/foundation.go deleted file mode 100644 index 422fcc60..00000000 --- a/foundation.go +++ /dev/null @@ -1,83 +0,0 @@ -package gorm - -import ( - "fmt" - "reflect" - "time" -) - -type foundation struct { - commonDialect -} - -func (foundation) BinVar(i int) string { - return fmt.Sprintf("$%v", i) -} - -func (foundation) SupportLastInsertId() bool { - return false -} - -func (foundation) SqlTag(value reflect.Value, size int, autoIncrease bool) string { - switch value.Kind() { - case reflect.Bool: - return "boolean" - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: - if autoIncrease { - return "serial" - } - return "int" - case reflect.Int64, reflect.Uint64: - if autoIncrease { - return "bigserial" - } - return "bigint" - case reflect.Float32, reflect.Float64: - return "double" - case reflect.String: - if size > 0 && size < 65532 { - return fmt.Sprintf("varchar(%d)", size) - } - return "clob" - case reflect.Struct: - if _, ok := value.Interface().(time.Time); ok { - return "datetime" - } - default: - if _, ok := value.Interface().([]byte); ok { - return "blob" - } - } - panic(fmt.Sprintf("invalid sql type %s (%s) for foundation", value.Type().Name(), value.Kind().String())) -} - -func (s foundation) ReturningStr(tableName, key string) string { - return fmt.Sprintf("RETURNING %v.%v", tableName, key) -} - -func (s foundation) HasTable(scope *Scope, tableName string) bool { - var count int - s.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.tables WHERE table_schema = current_schema AND table_type = 'TABLE' AND table_name = ?", tableName) - return count > 0 -} - -func (s foundation) HasColumn(scope *Scope, tableName string, columnName string) bool { - var count int - s.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.columns WHERE table_schema = current_schema AND table_name = ? AND column_name = ?", tableName, columnName) - return count > 0 -} - -func (s foundation) RemoveIndex(scope *Scope, indexName string) { - scope.NewDB().Exec(fmt.Sprintf("DROP INDEX %v", s.Quote(indexName))) -} - -func (s foundation) HasIndex(scope *Scope, tableName string, indexName string) bool { - var count int - s.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.indexes WHERE table_schema = current_schema AND table_name = ? AND index_name = ?", tableName, indexName) - return count > 0 -} - -func (s foundation) CurrentDatabase(scope *Scope) (name string) { - s.RawScanString(scope, &name, "SELECT CURRENT_SCHEMA") - return -} From de73d305032841fdc62a6762836bc8d701656ffe Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 17 Jan 2016 15:30:42 +0800 Subject: [PATCH 21/83] Refactor callbacks --- callback.go | 15 +++++++++++-- callback_create.go | 28 ++++++++++++------------- callback_delete.go | 16 +++++++------- callback_query.go | 10 ++++----- preload.go => callback_query_preload.go | 2 +- callback_shared.go | 8 +++---- callback_update.go | 28 ++++++++++++------------- 7 files changed, 59 insertions(+), 48 deletions(-) rename preload.go => callback_query_preload.go (99%) diff --git a/callback.go b/callback.go index 55cd807e..6d0ff0f3 100644 --- a/callback.go +++ b/callback.go @@ -23,7 +23,7 @@ type Callback struct { processors []*CallbackProcessor } -// callbackProcessor contains all informations for a callback +// CallbackProcessor contains all informations for a callback type CallbackProcessor struct { name string // current callback's name before string // register current callback before a callback @@ -79,7 +79,7 @@ func (c *Callback) Query() *CallbackProcessor { return c.addProcessor("query") } -// Query could be used to register callbacks for querying objects with `Row`, `Rows`, refer `Create` for usage +// RowQuery could be used to register callbacks for querying objects with `Row`, `Rows`, refer `Create` for usage func (c *Callback) RowQuery() *CallbackProcessor { return c.addProcessor("row_query") } @@ -125,6 +125,17 @@ func (cp *CallbackProcessor) Replace(callbackName string, callback func(scope *S cp.parent.reorder() } +// Get registered callback +// db.Callback().Create().Get("gorm:create") +func (cp *CallbackProcessor) Get(callbackName string) (callback func(scope *Scope)) { + for _, processor := range cp.parent.processors { + if processor.name == callbackName && processor.kind == cp.kind && !cp.remove { + return *cp.processor + } + } + return nil +} + // getRIndex get right index from string slice func getRIndex(strs []string, str string) int { for i := len(strs) - 1; i >= 0; i-- { diff --git a/callback_create.go b/callback_create.go index c52b9c85..3003b3a5 100644 --- a/callback_create.go +++ b/callback_create.go @@ -5,12 +5,12 @@ import ( "strings" ) -func BeforeCreate(scope *Scope) { +func beforeCreateCallback(scope *Scope) { scope.CallMethodWithErrorCheck("BeforeSave") scope.CallMethodWithErrorCheck("BeforeCreate") } -func UpdateTimeStampWhenCreate(scope *Scope) { +func updateTimeStampForCreateCallback(scope *Scope) { if !scope.HasError() { now := NowFunc() scope.SetColumn("CreatedAt", now) @@ -18,7 +18,7 @@ func UpdateTimeStampWhenCreate(scope *Scope) { } } -func Create(scope *Scope) { +func createCallback(scope *Scope) { defer scope.trace(NowFunc()) if !scope.HasError() { @@ -102,25 +102,25 @@ func Create(scope *Scope) { } } -func ForceReloadAfterCreate(scope *Scope) { +func forceReloadAfterCreateCallback(scope *Scope) { if columns, ok := scope.InstanceGet("gorm:force_reload_after_create_attrs"); ok { scope.DB().New().Select(columns.([]string)).First(scope.Value) } } -func AfterCreate(scope *Scope) { +func afterCreateCallback(scope *Scope) { scope.CallMethodWithErrorCheck("AfterCreate") scope.CallMethodWithErrorCheck("AfterSave") } func init() { - defaultCallback.Create().Register("gorm:begin_transaction", BeginTransaction) - defaultCallback.Create().Register("gorm:before_create", BeforeCreate) - defaultCallback.Create().Register("gorm:save_before_associations", SaveBeforeAssociations) - defaultCallback.Create().Register("gorm:update_time_stamp_when_create", UpdateTimeStampWhenCreate) - defaultCallback.Create().Register("gorm:create", Create) - defaultCallback.Create().Register("gorm:force_reload_after_create", ForceReloadAfterCreate) - defaultCallback.Create().Register("gorm:save_after_associations", SaveAfterAssociations) - defaultCallback.Create().Register("gorm:after_create", AfterCreate) - defaultCallback.Create().Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) + defaultCallback.Create().Register("gorm:begin_transaction", beginTransactionCallback) + defaultCallback.Create().Register("gorm:before_create", beforeCreateCallback) + defaultCallback.Create().Register("gorm:save_before_associations", saveBeforeAssociationsCallback) + defaultCallback.Create().Register("gorm:update_time_stamp_when_create", updateTimeStampForCreateCallback) + defaultCallback.Create().Register("gorm:create", createCallback) + defaultCallback.Create().Register("gorm:force_reload_after_create", forceReloadAfterCreateCallback) + defaultCallback.Create().Register("gorm:save_after_associations", saveAfterAssociationsCallback) + defaultCallback.Create().Register("gorm:after_create", afterCreateCallback) + defaultCallback.Create().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback) } diff --git a/callback_delete.go b/callback_delete.go index dca6ee21..7616cb87 100644 --- a/callback_delete.go +++ b/callback_delete.go @@ -2,11 +2,11 @@ package gorm import "fmt" -func BeforeDelete(scope *Scope) { +func beforeDeleteCallback(scope *Scope) { scope.CallMethodWithErrorCheck("BeforeDelete") } -func Delete(scope *Scope) { +func deleteCallback(scope *Scope) { if !scope.HasError() { if !scope.Search.Unscoped && scope.HasColumn("DeletedAt") { scope.Raw( @@ -23,14 +23,14 @@ func Delete(scope *Scope) { } } -func AfterDelete(scope *Scope) { +func afterDeleteCallback(scope *Scope) { scope.CallMethodWithErrorCheck("AfterDelete") } func init() { - defaultCallback.Delete().Register("gorm:begin_transaction", BeginTransaction) - defaultCallback.Delete().Register("gorm:before_delete", BeforeDelete) - defaultCallback.Delete().Register("gorm:delete", Delete) - defaultCallback.Delete().Register("gorm:after_delete", AfterDelete) - defaultCallback.Delete().Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) + defaultCallback.Delete().Register("gorm:begin_transaction", beginTransactionCallback) + defaultCallback.Delete().Register("gorm:before_delete", beforeDeleteCallback) + defaultCallback.Delete().Register("gorm:delete", deleteCallback) + defaultCallback.Delete().Register("gorm:after_delete", afterDeleteCallback) + defaultCallback.Delete().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback) } diff --git a/callback_query.go b/callback_query.go index 05ac8880..707d83f5 100644 --- a/callback_query.go +++ b/callback_query.go @@ -6,7 +6,7 @@ import ( "reflect" ) -func Query(scope *Scope) { +func queryCallback(scope *Scope) { defer scope.trace(NowFunc()) var ( @@ -78,12 +78,12 @@ func Query(scope *Scope) { } } -func AfterQuery(scope *Scope) { +func afterQueryCallback(scope *Scope) { scope.CallMethodWithErrorCheck("AfterFind") } func init() { - defaultCallback.Query().Register("gorm:query", Query) - defaultCallback.Query().Register("gorm:after_query", AfterQuery) - defaultCallback.Query().Register("gorm:preload", Preload) + defaultCallback.Query().Register("gorm:query", queryCallback) + defaultCallback.Query().Register("gorm:after_query", afterQueryCallback) + defaultCallback.Query().Register("gorm:preload", preloadCallback) } diff --git a/preload.go b/callback_query_preload.go similarity index 99% rename from preload.go rename to callback_query_preload.go index 692280ef..5dc91de9 100644 --- a/preload.go +++ b/callback_query_preload.go @@ -7,7 +7,7 @@ import ( "strings" ) -func Preload(scope *Scope) { +func preloadCallback(scope *Scope) { if scope.Search.preload == nil || scope.HasError() { return } diff --git a/callback_shared.go b/callback_shared.go index 547059e3..a525b709 100644 --- a/callback_shared.go +++ b/callback_shared.go @@ -2,15 +2,15 @@ package gorm import "reflect" -func BeginTransaction(scope *Scope) { +func beginTransactionCallback(scope *Scope) { scope.Begin() } -func CommitOrRollbackTransaction(scope *Scope) { +func commitOrRollbackTransactionCallback(scope *Scope) { scope.CommitOrRollback() } -func SaveBeforeAssociations(scope *Scope) { +func saveBeforeAssociationsCallback(scope *Scope) { if !scope.shouldSaveAssociations() { return } @@ -32,7 +32,7 @@ func SaveBeforeAssociations(scope *Scope) { } } -func SaveAfterAssociations(scope *Scope) { +func saveAfterAssociationsCallback(scope *Scope) { if !scope.shouldSaveAssociations() { return } diff --git a/callback_update.go b/callback_update.go index e7884450..f8ded58b 100644 --- a/callback_update.go +++ b/callback_update.go @@ -5,7 +5,7 @@ import ( "strings" ) -func AssignUpdateAttributes(scope *Scope) { +func assignUpdateAttributesCallback(scope *Scope) { if attrs, ok := scope.InstanceGet("gorm:update_interface"); ok { if maps := convertInterfaceToMap(attrs); len(maps) > 0 { protected, ok := scope.Get("gorm:ignore_protected_attrs") @@ -24,20 +24,20 @@ func AssignUpdateAttributes(scope *Scope) { } } -func BeforeUpdate(scope *Scope) { +func beforeUpdateCallback(scope *Scope) { if _, ok := scope.Get("gorm:update_column"); !ok { scope.CallMethodWithErrorCheck("BeforeSave") scope.CallMethodWithErrorCheck("BeforeUpdate") } } -func UpdateTimeStampWhenUpdate(scope *Scope) { +func updateTimeStampForUpdateCallback(scope *Scope) { if _, ok := scope.Get("gorm:update_column"); !ok { scope.SetColumn("UpdatedAt", NowFunc()) } } -func Update(scope *Scope) { +func updateCallback(scope *Scope) { if !scope.HasError() { var sqls []string @@ -75,7 +75,7 @@ func Update(scope *Scope) { } } -func AfterUpdate(scope *Scope) { +func afterUpdateCallback(scope *Scope) { if _, ok := scope.Get("gorm:update_column"); !ok { scope.CallMethodWithErrorCheck("AfterUpdate") scope.CallMethodWithErrorCheck("AfterSave") @@ -83,13 +83,13 @@ func AfterUpdate(scope *Scope) { } func init() { - defaultCallback.Update().Register("gorm:assign_update_attributes", AssignUpdateAttributes) - defaultCallback.Update().Register("gorm:begin_transaction", BeginTransaction) - defaultCallback.Update().Register("gorm:before_update", BeforeUpdate) - defaultCallback.Update().Register("gorm:save_before_associations", SaveBeforeAssociations) - defaultCallback.Update().Register("gorm:update_time_stamp_when_update", UpdateTimeStampWhenUpdate) - defaultCallback.Update().Register("gorm:update", Update) - defaultCallback.Update().Register("gorm:save_after_associations", SaveAfterAssociations) - defaultCallback.Update().Register("gorm:after_update", AfterUpdate) - defaultCallback.Update().Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) + defaultCallback.Update().Register("gorm:assign_update_attributes", assignUpdateAttributesCallback) + defaultCallback.Update().Register("gorm:begin_transaction", beginTransactionCallback) + defaultCallback.Update().Register("gorm:before_update", beforeUpdateCallback) + defaultCallback.Update().Register("gorm:save_before_associations", saveBeforeAssociationsCallback) + defaultCallback.Update().Register("gorm:update_time_stamp_when_update", updateTimeStampForUpdateCallback) + defaultCallback.Update().Register("gorm:update", updateCallback) + defaultCallback.Update().Register("gorm:save_after_associations", saveAfterAssociationsCallback) + defaultCallback.Update().Register("gorm:after_update", afterUpdateCallback) + defaultCallback.Update().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback) } From 31366f388f5e625e911d141c4eb890b64aaf30b1 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 17 Jan 2016 16:14:14 +0800 Subject: [PATCH 22/83] Remove uncessary method CallMethodWithErrorCheck for Scope --- callback_create.go | 16 ++++++++++++---- callback_delete.go | 8 ++++++-- callback_query.go | 4 +++- callback_update.go | 16 ++++++++++++---- scope.go | 8 ++------ 5 files changed, 35 insertions(+), 17 deletions(-) diff --git a/callback_create.go b/callback_create.go index 3003b3a5..6054aa91 100644 --- a/callback_create.go +++ b/callback_create.go @@ -6,8 +6,12 @@ import ( ) func beforeCreateCallback(scope *Scope) { - scope.CallMethodWithErrorCheck("BeforeSave") - scope.CallMethodWithErrorCheck("BeforeCreate") + if !scope.HasError() { + scope.CallMethod("BeforeSave") + } + if !scope.HasError() { + scope.CallMethod("BeforeCreate") + } } func updateTimeStampForCreateCallback(scope *Scope) { @@ -109,8 +113,12 @@ func forceReloadAfterCreateCallback(scope *Scope) { } func afterCreateCallback(scope *Scope) { - scope.CallMethodWithErrorCheck("AfterCreate") - scope.CallMethodWithErrorCheck("AfterSave") + if !scope.HasError() { + scope.CallMethod("AfterCreate") + } + if !scope.HasError() { + scope.CallMethod("AfterSave") + } } func init() { diff --git a/callback_delete.go b/callback_delete.go index 7616cb87..804bdf71 100644 --- a/callback_delete.go +++ b/callback_delete.go @@ -3,7 +3,9 @@ package gorm import "fmt" func beforeDeleteCallback(scope *Scope) { - scope.CallMethodWithErrorCheck("BeforeDelete") + if !scope.HasError() { + scope.CallMethod("BeforeDelete") + } } func deleteCallback(scope *Scope) { @@ -24,7 +26,9 @@ func deleteCallback(scope *Scope) { } func afterDeleteCallback(scope *Scope) { - scope.CallMethodWithErrorCheck("AfterDelete") + if !scope.HasError() { + scope.CallMethod("AfterDelete") + } } func init() { diff --git a/callback_query.go b/callback_query.go index 707d83f5..9ebff6b7 100644 --- a/callback_query.go +++ b/callback_query.go @@ -79,7 +79,9 @@ func queryCallback(scope *Scope) { } func afterQueryCallback(scope *Scope) { - scope.CallMethodWithErrorCheck("AfterFind") + if !scope.HasError() { + scope.CallMethod("AfterFind") + } } func init() { diff --git a/callback_update.go b/callback_update.go index f8ded58b..9463c994 100644 --- a/callback_update.go +++ b/callback_update.go @@ -26,8 +26,12 @@ func assignUpdateAttributesCallback(scope *Scope) { func beforeUpdateCallback(scope *Scope) { if _, ok := scope.Get("gorm:update_column"); !ok { - scope.CallMethodWithErrorCheck("BeforeSave") - scope.CallMethodWithErrorCheck("BeforeUpdate") + if !scope.HasError() { + scope.CallMethod("BeforeSave") + } + if !scope.HasError() { + scope.CallMethod("BeforeUpdate") + } } } @@ -77,8 +81,12 @@ func updateCallback(scope *Scope) { func afterUpdateCallback(scope *Scope) { if _, ok := scope.Get("gorm:update_column"); !ok { - scope.CallMethodWithErrorCheck("AfterUpdate") - scope.CallMethodWithErrorCheck("AfterSave") + if !scope.HasError() { + scope.CallMethod("AfterUpdate") + } + if !scope.HasError() { + scope.CallMethod("AfterSave") + } } } diff --git a/scope.go b/scope.go index 0d6602b5..b2a2852b 100644 --- a/scope.go +++ b/scope.go @@ -192,8 +192,8 @@ func (scope *Scope) SetColumn(column interface{}, value interface{}) error { return errors.New("could not convert column to field") } -func (scope *Scope) CallMethod(name string, checkError bool) { - if scope.Value == nil || (checkError && scope.HasError()) { +func (scope *Scope) CallMethod(name string) { + if scope.Value == nil { return } @@ -239,10 +239,6 @@ func (scope *Scope) CallMethod(name string, checkError bool) { } } -func (scope *Scope) CallMethodWithErrorCheck(name string) { - scope.CallMethod(name, true) -} - // AddToVars add value as sql's vars, gorm will escape them func (scope *Scope) AddToVars(value interface{}) string { if expr, ok := value.(*expr); ok { From 317e1a9a48981563a6cf940a98640916adaa51ba Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 17 Jan 2016 16:28:32 +0800 Subject: [PATCH 23/83] Keep refactoring callbacks --- callback_create.go | 30 +++++++++++++++----------- callback_delete.go | 16 +++++++------- callback_query.go | 12 +++++------ callback_shared.go => callback_save.go | 0 callback_update.go | 24 ++++++++++----------- 5 files changed, 44 insertions(+), 38 deletions(-) rename callback_shared.go => callback_save.go (100%) diff --git a/callback_create.go b/callback_create.go index 6054aa91..11082f8a 100644 --- a/callback_create.go +++ b/callback_create.go @@ -5,6 +5,20 @@ import ( "strings" ) +// Define callbacks for creating +func init() { + defaultCallback.Create().Register("gorm:begin_transaction", beginTransactionCallback) + defaultCallback.Create().Register("gorm:before_create", beforeCreateCallback) + defaultCallback.Create().Register("gorm:save_before_associations", saveBeforeAssociationsCallback) + defaultCallback.Create().Register("gorm:update_time_stamp_when_create", updateTimeStampForCreateCallback) + defaultCallback.Create().Register("gorm:create", createCallback) + defaultCallback.Create().Register("gorm:force_reload_after_create", forceReloadAfterCreateCallback) + defaultCallback.Create().Register("gorm:save_after_associations", saveAfterAssociationsCallback) + defaultCallback.Create().Register("gorm:after_create", afterCreateCallback) + defaultCallback.Create().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback) +} + +// beforeCreateCallback will invoke `BeforeSave`, `BeforeCreate` method before creating func beforeCreateCallback(scope *Scope) { if !scope.HasError() { scope.CallMethod("BeforeSave") @@ -14,6 +28,7 @@ func beforeCreateCallback(scope *Scope) { } } +// updateTimeStampForCreateCallback will set `CreatedAt`, `UpdatedAt` when creating func updateTimeStampForCreateCallback(scope *Scope) { if !scope.HasError() { now := NowFunc() @@ -22,6 +37,7 @@ func updateTimeStampForCreateCallback(scope *Scope) { } } +// createCallback the callback used to insert data into database func createCallback(scope *Scope) { defer scope.trace(NowFunc()) @@ -106,12 +122,14 @@ func createCallback(scope *Scope) { } } +// forceReloadAfterCreateCallback will reload columns that having default value, and set it back to current object func forceReloadAfterCreateCallback(scope *Scope) { if columns, ok := scope.InstanceGet("gorm:force_reload_after_create_attrs"); ok { scope.DB().New().Select(columns.([]string)).First(scope.Value) } } +// beforeCreateCallback will invoke `AfterCreate`, `AfterSave` method after creating func afterCreateCallback(scope *Scope) { if !scope.HasError() { scope.CallMethod("AfterCreate") @@ -120,15 +138,3 @@ func afterCreateCallback(scope *Scope) { scope.CallMethod("AfterSave") } } - -func init() { - defaultCallback.Create().Register("gorm:begin_transaction", beginTransactionCallback) - defaultCallback.Create().Register("gorm:before_create", beforeCreateCallback) - defaultCallback.Create().Register("gorm:save_before_associations", saveBeforeAssociationsCallback) - defaultCallback.Create().Register("gorm:update_time_stamp_when_create", updateTimeStampForCreateCallback) - defaultCallback.Create().Register("gorm:create", createCallback) - defaultCallback.Create().Register("gorm:force_reload_after_create", forceReloadAfterCreateCallback) - defaultCallback.Create().Register("gorm:save_after_associations", saveAfterAssociationsCallback) - defaultCallback.Create().Register("gorm:after_create", afterCreateCallback) - defaultCallback.Create().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback) -} diff --git a/callback_delete.go b/callback_delete.go index 804bdf71..17b5cfb4 100644 --- a/callback_delete.go +++ b/callback_delete.go @@ -2,6 +2,14 @@ package gorm import "fmt" +func init() { + defaultCallback.Delete().Register("gorm:begin_transaction", beginTransactionCallback) + defaultCallback.Delete().Register("gorm:before_delete", beforeDeleteCallback) + defaultCallback.Delete().Register("gorm:delete", deleteCallback) + defaultCallback.Delete().Register("gorm:after_delete", afterDeleteCallback) + defaultCallback.Delete().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback) +} + func beforeDeleteCallback(scope *Scope) { if !scope.HasError() { scope.CallMethod("BeforeDelete") @@ -30,11 +38,3 @@ func afterDeleteCallback(scope *Scope) { scope.CallMethod("AfterDelete") } } - -func init() { - defaultCallback.Delete().Register("gorm:begin_transaction", beginTransactionCallback) - defaultCallback.Delete().Register("gorm:before_delete", beforeDeleteCallback) - defaultCallback.Delete().Register("gorm:delete", deleteCallback) - defaultCallback.Delete().Register("gorm:after_delete", afterDeleteCallback) - defaultCallback.Delete().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback) -} diff --git a/callback_query.go b/callback_query.go index 9ebff6b7..f6fa0aa1 100644 --- a/callback_query.go +++ b/callback_query.go @@ -6,6 +6,12 @@ import ( "reflect" ) +func init() { + defaultCallback.Query().Register("gorm:query", queryCallback) + defaultCallback.Query().Register("gorm:after_query", afterQueryCallback) + defaultCallback.Query().Register("gorm:preload", preloadCallback) +} + func queryCallback(scope *Scope) { defer scope.trace(NowFunc()) @@ -83,9 +89,3 @@ func afterQueryCallback(scope *Scope) { scope.CallMethod("AfterFind") } } - -func init() { - defaultCallback.Query().Register("gorm:query", queryCallback) - defaultCallback.Query().Register("gorm:after_query", afterQueryCallback) - defaultCallback.Query().Register("gorm:preload", preloadCallback) -} diff --git a/callback_shared.go b/callback_save.go similarity index 100% rename from callback_shared.go rename to callback_save.go diff --git a/callback_update.go b/callback_update.go index 9463c994..b9d2bcbc 100644 --- a/callback_update.go +++ b/callback_update.go @@ -5,6 +5,18 @@ import ( "strings" ) +func init() { + defaultCallback.Update().Register("gorm:assign_update_attributes", assignUpdateAttributesCallback) + defaultCallback.Update().Register("gorm:begin_transaction", beginTransactionCallback) + defaultCallback.Update().Register("gorm:before_update", beforeUpdateCallback) + defaultCallback.Update().Register("gorm:save_before_associations", saveBeforeAssociationsCallback) + defaultCallback.Update().Register("gorm:update_time_stamp_when_update", updateTimeStampForUpdateCallback) + defaultCallback.Update().Register("gorm:update", updateCallback) + defaultCallback.Update().Register("gorm:save_after_associations", saveAfterAssociationsCallback) + defaultCallback.Update().Register("gorm:after_update", afterUpdateCallback) + defaultCallback.Update().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback) +} + func assignUpdateAttributesCallback(scope *Scope) { if attrs, ok := scope.InstanceGet("gorm:update_interface"); ok { if maps := convertInterfaceToMap(attrs); len(maps) > 0 { @@ -89,15 +101,3 @@ func afterUpdateCallback(scope *Scope) { } } } - -func init() { - defaultCallback.Update().Register("gorm:assign_update_attributes", assignUpdateAttributesCallback) - defaultCallback.Update().Register("gorm:begin_transaction", beginTransactionCallback) - defaultCallback.Update().Register("gorm:before_update", beforeUpdateCallback) - defaultCallback.Update().Register("gorm:save_before_associations", saveBeforeAssociationsCallback) - defaultCallback.Update().Register("gorm:update_time_stamp_when_update", updateTimeStampForUpdateCallback) - defaultCallback.Update().Register("gorm:update", updateCallback) - defaultCallback.Update().Register("gorm:save_after_associations", saveAfterAssociationsCallback) - defaultCallback.Update().Register("gorm:after_update", afterUpdateCallback) - defaultCallback.Update().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback) -} From 4f84bf0d94988d4b9f0b4f26386d825d5268703b Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 17 Jan 2016 16:37:17 +0800 Subject: [PATCH 24/83] Hide method QuoteIfPossible --- callback_create.go | 4 ++-- scope.go | 2 +- scope_private.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/callback_create.go b/callback_create.go index 11082f8a..7e04d67d 100644 --- a/callback_create.go +++ b/callback_create.go @@ -39,9 +39,9 @@ func updateTimeStampForCreateCallback(scope *Scope) { // createCallback the callback used to insert data into database func createCallback(scope *Scope) { - defer scope.trace(NowFunc()) - if !scope.HasError() { + defer scope.trace(NowFunc()) + // set create sql var sqls, columns []string fields := scope.Fields() diff --git a/scope.go b/scope.go index b2a2852b..0abfa132 100644 --- a/scope.go +++ b/scope.go @@ -88,7 +88,7 @@ func (scope *Scope) Quote(str string) string { return scope.Dialect().Quote(str) } -func (scope *Scope) QuoteIfPossible(str string) string { +func (scope *Scope) quoteIfPossible(str string) string { if regexp.MustCompile("^[a-zA-Z]+(.[a-zA-Z]+)*$").MatchString(str) { return scope.Quote(str) } diff --git a/scope_private.go b/scope_private.go index 769d4c64..e0a98c1c 100644 --- a/scope_private.go +++ b/scope_private.go @@ -663,7 +663,7 @@ func (scope *Scope) addIndex(unique bool, indexName string, column ...string) { var columns []string for _, name := range column { - columns = append(columns, scope.QuoteIfPossible(name)) + columns = append(columns, scope.quoteIfPossible(name)) } sqlCreate := "CREATE INDEX" @@ -678,7 +678,7 @@ func (scope *Scope) addForeignKey(field string, dest string, onDelete string, on var keyName = fmt.Sprintf("%s_%s_%s_foreign", scope.TableName(), field, dest) keyName = regexp.MustCompile("(_*[^a-zA-Z]+_*|_+)").ReplaceAllString(keyName, "_") var query = `ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s ON DELETE %s ON UPDATE %s;` - scope.Raw(fmt.Sprintf(query, scope.QuotedTableName(), scope.QuoteIfPossible(keyName), scope.QuoteIfPossible(field), dest, onDelete, onUpdate)).Exec() + scope.Raw(fmt.Sprintf(query, scope.QuotedTableName(), scope.quoteIfPossible(keyName), scope.quoteIfPossible(field), dest, onDelete, onUpdate)).Exec() } func (scope *Scope) removeIndex(indexName string) { From e38b1e09486eeee61db349df7e771cecd0e3e2b2 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 17 Jan 2016 17:12:19 +0800 Subject: [PATCH 25/83] Remove uncessary NeedPtr method --- main.go | 2 +- scope.go | 10 ---------- utils_private.go | 3 +-- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/main.go b/main.go index d580c5a5..a56f282f 100644 --- a/main.go +++ b/main.go @@ -418,7 +418,7 @@ func (s *DB) HasTable(value interface{}) bool { func (s *DB) AutoMigrate(values ...interface{}) *DB { db := s.clone() for _, value := range values { - db = db.NewScope(value).NeedPtr().autoMigrate().db + db = db.NewScope(value).autoMigrate().db } return db } diff --git a/scope.go b/scope.go index 0abfa132..b0c2432c 100644 --- a/scope.go +++ b/scope.go @@ -35,16 +35,6 @@ func (scope *Scope) IndirectValue() reflect.Value { return *scope.indirectValue } -func (scope *Scope) NeedPtr() *Scope { - reflectKind := reflect.ValueOf(scope.Value).Kind() - if !((reflectKind == reflect.Invalid) || (reflectKind == reflect.Ptr)) { - err := fmt.Errorf("%v %v\n", fileWithLineNum(), "using unaddressable value") - scope.Err(err) - fmt.Printf(err.Error()) - } - return scope -} - // New create a new Scope without search information func (scope *Scope) New(value interface{}) *Scope { return &Scope{db: scope.NewDB(), Search: &search{}, Value: value} diff --git a/utils_private.go b/utils_private.go index c4f0e963..5c17eda5 100644 --- a/utils_private.go +++ b/utils_private.go @@ -63,8 +63,7 @@ func convertInterfaceToMap(values interface{}) map[string]interface{} { attrs[ToDBName(key.Interface().(string))] = reflectValue.MapIndex(key).Interface() } default: - scope := Scope{Value: values} - for _, field := range scope.Fields() { + for _, field := range (&Scope{Value: values}).Fields() { if !field.IsBlank && !field.IsIgnored { attrs[field.DBName] = field.Field.Interface() } From 92213273a5dccd52da5ed93ad1ef283af1691e57 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 17 Jan 2016 17:46:56 +0800 Subject: [PATCH 26/83] Refactor create callback --- callback_create.go | 69 ++++++++++++++++++++-------------------------- field.go | 28 ++++++++----------- utils.go | 10 +++---- 3 files changed, 46 insertions(+), 61 deletions(-) diff --git a/callback_create.go b/callback_create.go index 7e04d67d..921aa4cc 100644 --- a/callback_create.go +++ b/callback_create.go @@ -42,30 +42,29 @@ func createCallback(scope *Scope) { if !scope.HasError() { defer scope.trace(NowFunc()) - // set create sql - var sqls, columns []string - fields := scope.Fields() + var ( + columns, placeholders []string + blankColumnsWithDefaultValue []string + fields = scope.Fields() + ) + for _, field := range fields { if scope.changeableField(field) { if field.IsNormal { - if !field.IsPrimaryKey || (field.IsPrimaryKey && !field.IsBlank) { - if !field.IsBlank || !field.HasDefaultValue { + if !field.IsPrimaryKey || !field.IsBlank { + if field.IsBlank && field.HasDefaultValue { + blankColumnsWithDefaultValue = append(blankColumnsWithDefaultValue, field.DBName) + scope.InstanceSet("gorm:blank_columns_with_default_value", blankColumnsWithDefaultValue) + } else { columns = append(columns, scope.Quote(field.DBName)) - sqls = append(sqls, scope.AddToVars(field.Field.Interface())) - } else if field.HasDefaultValue { - var hasDefaultValueColumns []string - if oldHasDefaultValueColumns, ok := scope.InstanceGet("gorm:force_reload_after_create_attrs"); ok { - hasDefaultValueColumns = oldHasDefaultValueColumns.([]string) - } - hasDefaultValueColumns = append(hasDefaultValueColumns, field.DBName) - scope.InstanceSet("gorm:force_reload_after_create_attrs", hasDefaultValueColumns) + placeholders = append(placeholders, scope.AddToVars(field.Field.Interface())) } } - } else if relationship := field.Relationship; relationship != nil && relationship.Kind == "belongs_to" { - for _, dbName := range relationship.ForeignDBNames { - if relationField := fields[dbName]; !scope.changeableField(relationField) { - columns = append(columns, scope.Quote(relationField.DBName)) - sqls = append(sqls, scope.AddToVars(relationField.Field.Interface())) + } else if field.Relationship != nil && field.Relationship.Kind == "belongs_to" { + for _, foreignKey := range field.Relationship.ForeignDBNames { + if foreignField := fields[foreignKey]; !scope.changeableField(foreignField) { + columns = append(columns, scope.Quote(foreignField.DBName)) + placeholders = append(placeholders, scope.AddToVars(foreignField.Field.Interface())) } } } @@ -88,35 +87,27 @@ func createCallback(scope *Scope) { "INSERT INTO %v (%v) VALUES (%v) %v", scope.QuotedTableName(), strings.Join(columns, ","), - strings.Join(sqls, ","), + strings.Join(placeholders, ","), scope.Dialect().ReturningStr(scope.QuotedTableName(), returningKey), )) } // execute create sql - if scope.Dialect().SupportLastInsertId() { + if scope.Dialect().SupportLastInsertId() || primaryField == nil { if result, err := scope.SqlDB().Exec(scope.Sql, scope.SqlVars...); scope.Err(err) == nil { - id, err := result.LastInsertId() - if scope.Err(err) == nil { - scope.db.RowsAffected, _ = result.RowsAffected() - if primaryField != nil && primaryField.IsBlank { - scope.Err(scope.SetColumn(primaryField, id)) + // set rows affected count + scope.db.RowsAffected, _ = result.RowsAffected() + + // set primary value to primary field + if primaryField != nil && primaryField.IsBlank { + if primaryValue, err := result.LastInsertId(); scope.Err(err) == nil { + scope.Err(primaryField.Set(primaryValue)) } } } } else { - if primaryField == nil { - if results, err := scope.SqlDB().Exec(scope.Sql, scope.SqlVars...); err == nil { - scope.db.RowsAffected, _ = results.RowsAffected() - } else { - scope.Err(err) - } - } else { - if err := scope.Err(scope.SqlDB().QueryRow(scope.Sql, scope.SqlVars...).Scan(primaryField.Field.Addr().Interface())); err == nil { - scope.db.RowsAffected = 1 - } else { - scope.Err(err) - } + if err := scope.SqlDB().QueryRow(scope.Sql, scope.SqlVars...).Scan(primaryField.Field.Addr().Interface()); scope.Err(err) == nil { + scope.db.RowsAffected = 1 } } } @@ -124,8 +115,8 @@ func createCallback(scope *Scope) { // forceReloadAfterCreateCallback will reload columns that having default value, and set it back to current object func forceReloadAfterCreateCallback(scope *Scope) { - if columns, ok := scope.InstanceGet("gorm:force_reload_after_create_attrs"); ok { - scope.DB().New().Select(columns.([]string)).First(scope.Value) + if blankColumnsWithDefaultValue, ok := scope.InstanceGet("gorm:blank_columns_with_default_value"); ok { + scope.DB().New().Select(blankColumnsWithDefaultValue.([]string)).First(scope.Value) } } diff --git a/field.go b/field.go index 2ed4e732..2f0daf77 100644 --- a/field.go +++ b/field.go @@ -58,15 +58,20 @@ func (field *Field) Set(value interface{}) (err error) { // Fields get value's fields func (scope *Scope) Fields() map[string]*Field { if scope.fields == nil { - fields := map[string]*Field{} - modelStruct := scope.GetModelStruct() + var ( + fields = map[string]*Field{} + indirectScopeValue = scope.IndirectValue() + isStruct = indirectScopeValue.Kind() == reflect.Struct + ) - indirectValue := scope.IndirectValue() - isStruct := indirectValue.Kind() == reflect.Struct - for _, structField := range modelStruct.StructFields { + for _, structField := range scope.GetModelStruct().StructFields { if field, ok := fields[structField.DBName]; !ok || field.IsIgnored { if isStruct { - fields[structField.DBName] = getField(indirectValue, structField) + fieldValue := indirectScopeValue + for _, name := range structField.Names { + fieldValue = reflect.Indirect(fieldValue).FieldByName(name) + } + fields[structField.DBName] = &Field{StructField: structField, Field: fieldValue, IsBlank: isBlank(fieldValue)} } else { fields[structField.DBName] = &Field{StructField: structField, IsBlank: true} } @@ -74,17 +79,6 @@ func (scope *Scope) Fields() map[string]*Field { } scope.fields = fields - return fields } return scope.fields } - -func getField(indirectValue reflect.Value, structField *StructField) *Field { - field := &Field{StructField: structField} - for _, name := range structField.Names { - indirectValue = reflect.Indirect(indirectValue).FieldByName(name) - } - field.Field = indirectValue - field.IsBlank = isBlank(indirectValue) - return field -} diff --git a/utils.go b/utils.go index 58b14ac4..43d0031c 100644 --- a/utils.go +++ b/utils.go @@ -132,11 +132,11 @@ func toQueryCondition(scope *Scope, columns []string) string { return strings.Join(newColumns, ",") } -func toQueryValues(primaryValues [][]interface{}) (values []interface{}) { - for _, primaryValue := range primaryValues { - for _, value := range primaryValue { - values = append(values, value) +func toQueryValues(values [][]interface{}) (results []interface{}) { + for _, value := range values { + for _, v := range value { + results = append(results, v) } } - return values + return } From 58a7252251a205976d6fc0a99aeab9e6965fe9a7 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 17 Jan 2016 18:38:18 +0800 Subject: [PATCH 27/83] Refactor update callback --- callback_create.go | 4 ++-- callback_update.go | 46 +++++++++++++++++++++++++++------------------- scope_private.go | 31 +++---------------------------- utils_private.go | 2 +- 4 files changed, 33 insertions(+), 50 deletions(-) diff --git a/callback_create.go b/callback_create.go index 921aa4cc..b8725363 100644 --- a/callback_create.go +++ b/callback_create.go @@ -10,7 +10,7 @@ func init() { defaultCallback.Create().Register("gorm:begin_transaction", beginTransactionCallback) defaultCallback.Create().Register("gorm:before_create", beforeCreateCallback) defaultCallback.Create().Register("gorm:save_before_associations", saveBeforeAssociationsCallback) - defaultCallback.Create().Register("gorm:update_time_stamp_when_create", updateTimeStampForCreateCallback) + defaultCallback.Create().Register("gorm:update_time_stamp", updateTimeStampForCreateCallback) defaultCallback.Create().Register("gorm:create", createCallback) defaultCallback.Create().Register("gorm:force_reload_after_create", forceReloadAfterCreateCallback) defaultCallback.Create().Register("gorm:save_after_associations", saveAfterAssociationsCallback) @@ -120,7 +120,7 @@ func forceReloadAfterCreateCallback(scope *Scope) { } } -// beforeCreateCallback will invoke `AfterCreate`, `AfterSave` method after creating +// afterCreateCallback will invoke `AfterCreate`, `AfterSave` method after creating func afterCreateCallback(scope *Scope) { if !scope.HasError() { scope.CallMethod("AfterCreate") diff --git a/callback_update.go b/callback_update.go index b9d2bcbc..b3a6c7da 100644 --- a/callback_update.go +++ b/callback_update.go @@ -5,19 +5,21 @@ import ( "strings" ) +// Define callbacks for updating func init() { - defaultCallback.Update().Register("gorm:assign_update_attributes", assignUpdateAttributesCallback) + defaultCallback.Update().Register("gorm:assign_updating_attributes", assignUpdatingAttributesCallback) defaultCallback.Update().Register("gorm:begin_transaction", beginTransactionCallback) defaultCallback.Update().Register("gorm:before_update", beforeUpdateCallback) defaultCallback.Update().Register("gorm:save_before_associations", saveBeforeAssociationsCallback) - defaultCallback.Update().Register("gorm:update_time_stamp_when_update", updateTimeStampForUpdateCallback) + defaultCallback.Update().Register("gorm:update_time_stamp", updateTimeStampForUpdateCallback) defaultCallback.Update().Register("gorm:update", updateCallback) defaultCallback.Update().Register("gorm:save_after_associations", saveAfterAssociationsCallback) defaultCallback.Update().Register("gorm:after_update", afterUpdateCallback) defaultCallback.Update().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback) } -func assignUpdateAttributesCallback(scope *Scope) { +// assignUpdatingAttributesCallback assign updating attributes to model +func assignUpdatingAttributesCallback(scope *Scope) { if attrs, ok := scope.InstanceGet("gorm:update_interface"); ok { if maps := convertInterfaceToMap(attrs); len(maps) > 0 { protected, ok := scope.Get("gorm:ignore_protected_attrs") @@ -36,6 +38,7 @@ func assignUpdateAttributesCallback(scope *Scope) { } } +// beforeUpdateCallback will invoke `BeforeSave`, `BeforeUpdate` method before updating func beforeUpdateCallback(scope *Scope) { if _, ok := scope.Get("gorm:update_column"); !ok { if !scope.HasError() { @@ -47,32 +50,40 @@ func beforeUpdateCallback(scope *Scope) { } } +// updateTimeStampForUpdateCallback will set `UpdatedAt` when updating func updateTimeStampForUpdateCallback(scope *Scope) { if _, ok := scope.Get("gorm:update_column"); !ok { scope.SetColumn("UpdatedAt", NowFunc()) } } +// updateCallback the callback used to update data to database func updateCallback(scope *Scope) { if !scope.HasError() { var sqls []string if updateAttrs, ok := scope.InstanceGet("gorm:update_attrs"); ok { - for key, value := range updateAttrs.(map[string]interface{}) { - if scope.changeableDBColumn(key) { - sqls = append(sqls, fmt.Sprintf("%v = %v", scope.Quote(key), scope.AddToVars(value))) + for column, value := range updateAttrs.(map[string]interface{}) { + if field, ok := scope.FieldByName(column); ok { + if scope.changeableField(field) { + sqls = append(sqls, fmt.Sprintf("%v = %v", scope.Quote(field.DBName), scope.AddToVars(value))) + } + } else { + sqls = append(sqls, fmt.Sprintf("%v = %v", scope.Quote(column), scope.AddToVars(value))) } } } else { fields := scope.Fields() for _, field := range fields { - if scope.changeableField(field) && !field.IsPrimaryKey && field.IsNormal { - sqls = append(sqls, fmt.Sprintf("%v = %v", scope.Quote(field.DBName), scope.AddToVars(field.Field.Interface()))) - } else if relationship := field.Relationship; relationship != nil && relationship.Kind == "belongs_to" { - for _, dbName := range relationship.ForeignDBNames { - if relationField := fields[dbName]; !scope.changeableField(relationField) && !relationField.IsBlank { - sql := fmt.Sprintf("%v = %v", scope.Quote(relationField.DBName), scope.AddToVars(relationField.Field.Interface())) - sqls = append(sqls, sql) + if scope.changeableField(field) { + if !field.IsPrimaryKey && field.IsNormal { + sqls = append(sqls, fmt.Sprintf("%v = %v", scope.Quote(field.DBName), scope.AddToVars(field.Field.Interface()))) + } else if relationship := field.Relationship; relationship != nil && relationship.Kind == "belongs_to" { + for _, foreignKey := range relationship.ForeignDBNames { + if foreignField := fields[foreignKey]; !scope.changeableField(foreignField) { + sqls = append(sqls, + fmt.Sprintf("%v = %v", scope.Quote(foreignField.DBName), scope.AddToVars(foreignField.Field.Interface()))) + } } } } @@ -81,16 +92,13 @@ func updateCallback(scope *Scope) { if len(sqls) > 0 { scope.Raw(fmt.Sprintf( - "UPDATE %v SET %v %v", - scope.QuotedTableName(), - strings.Join(sqls, ", "), - scope.CombinedConditionSql(), - )) - scope.Exec() + "UPDATE %v SET %v %v", scope.QuotedTableName(), strings.Join(sqls, ", "), scope.CombinedConditionSql(), + )).Exec() } } } +// afterUpdateCallback will invoke `AfterUpdate`, `AfterSave` method after updating func afterUpdateCallback(scope *Scope) { if _, ok := scope.Get("gorm:update_column"); !ok { if !scope.HasError() { diff --git a/scope_private.go b/scope_private.go index e0a98c1c..ef16cf93 100644 --- a/scope_private.go +++ b/scope_private.go @@ -441,32 +441,8 @@ func (scope *Scope) trace(t time.Time) { } } -func (scope *Scope) changeableDBColumn(column string) bool { - selectAttrs := scope.SelectAttrs() - omitAttrs := scope.OmitAttrs() - - if len(selectAttrs) > 0 { - for _, attr := range selectAttrs { - if column == ToDBName(attr) { - return true - } - } - return false - } - - for _, attr := range omitAttrs { - if column == ToDBName(attr) { - return false - } - } - return true -} - func (scope *Scope) changeableField(field *Field) bool { - selectAttrs := scope.SelectAttrs() - omitAttrs := scope.OmitAttrs() - - if len(selectAttrs) > 0 { + if selectAttrs := scope.SelectAttrs(); len(selectAttrs) > 0 { for _, attr := range selectAttrs { if field.Name == attr || field.DBName == attr { return true @@ -475,7 +451,7 @@ func (scope *Scope) changeableField(field *Field) bool { return false } - for _, attr := range omitAttrs { + for _, attr := range scope.OmitAttrs() { if field.Name == attr || field.DBName == attr { return false } @@ -485,8 +461,7 @@ func (scope *Scope) changeableField(field *Field) bool { } func (scope *Scope) shouldSaveAssociations() bool { - saveAssociations, ok := scope.Get("gorm:save_associations") - if ok && !saveAssociations.(bool) { + if saveAssociations, ok := scope.Get("gorm:save_associations"); ok && !saveAssociations.(bool) { return false } return true && !scope.HasError() diff --git a/utils_private.go b/utils_private.go index 5c17eda5..2851a37e 100644 --- a/utils_private.go +++ b/utils_private.go @@ -46,7 +46,7 @@ func convertInterfaceToMap(values interface{}) map[string]interface{} { switch value := values.(type) { case map[string]interface{}: for k, v := range value { - attrs[ToDBName(k)] = v + attrs[k] = v } case []interface{}: for _, v := range value { From 07773cc367acc95997417cf0707c1e4d267246a6 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 17 Jan 2016 20:51:11 +0800 Subject: [PATCH 28/83] Keep refactoring on callbacks --- callback.go | 87 ++++++++++++++++++--------------------- callback_delete.go | 10 +++-- callback_query.go | 69 +++++++++++++++---------------- callback_query_preload.go | 5 +++ 4 files changed, 84 insertions(+), 87 deletions(-) diff --git a/callback.go b/callback.go index 6d0ff0f3..309078e4 100644 --- a/callback.go +++ b/callback.go @@ -148,75 +148,66 @@ func getRIndex(strs []string, str string) int { // sortProcessors sort callback processors based on its before, after, remove, replace func sortProcessors(cps []*CallbackProcessor) []*func(scope *Scope) { - var sortCallbackProcessor func(c *CallbackProcessor) - var names, sortedNames = []string{}, []string{} + var ( + allNames, sortedNames []string + sortCallbackProcessor func(c *CallbackProcessor) + ) for _, cp := range cps { - if index := getRIndex(names, cp.name); index > -1 { - if !cp.replace && !cp.remove { - fmt.Printf("[warning] duplicated callback `%v` from %v\n", cp.name, fileWithLineNum()) - } + // show warning message the callback name already exists + if index := getRIndex(allNames, cp.name); index > -1 && !cp.replace && !cp.remove { + fmt.Printf("[warning] duplicated callback `%v` from %v\n", cp.name, fileWithLineNum()) } - names = append(names, cp.name) + allNames = append(allNames, cp.name) } sortCallbackProcessor = func(c *CallbackProcessor) { - if getRIndex(sortedNames, c.name) > -1 { - return - } - - if len(c.before) > 0 { - if index := getRIndex(sortedNames, c.before); index > -1 { - sortedNames = append(sortedNames[:index], append([]string{c.name}, sortedNames[index:]...)...) - } else if index := getRIndex(names, c.before); index > -1 { - sortedNames = append(sortedNames, c.name) - sortCallbackProcessor(cps[index]) - } else { - sortedNames = append(sortedNames, c.name) - } - } - - if len(c.after) > 0 { - if index := getRIndex(sortedNames, c.after); index > -1 { - sortedNames = append(sortedNames[:index+1], append([]string{c.name}, sortedNames[index+1:]...)...) - } else if index := getRIndex(names, c.after); index > -1 { - cp := cps[index] - if len(cp.before) == 0 { - cp.before = c.name + if getRIndex(sortedNames, c.name) == -1 { // if not sorted + if c.before != "" { // if defined before callback + if index := getRIndex(sortedNames, c.before); index != -1 { + // if before callback already sorted, append current callback just after it + sortedNames = append(sortedNames[:index], append([]string{c.name}, sortedNames[index:]...)...) + } else if index := getRIndex(allNames, c.before); index != -1 { + // if before callback exists but haven't sorted, append current callback to last + sortedNames = append(sortedNames, c.name) + sortCallbackProcessor(cps[index]) } - sortCallbackProcessor(cp) - } else { + } + + if c.after != "" { // if defined after callback + if index := getRIndex(sortedNames, c.after); index != -1 { + // if after callback already sorted, append current callback just before it + sortedNames = append(sortedNames[:index+1], append([]string{c.name}, sortedNames[index+1:]...)...) + } else if index := getRIndex(allNames, c.after); index != -1 { + // if after callback exists but haven't sorted + cp := cps[index] + // set after callback's before callback to current callback + if cp.before == "" { + cp.before = c.name + } + sortCallbackProcessor(cp) + } + } + + // if current callback haven't been sorted, append it to last + if getRIndex(sortedNames, c.name) == -1 { sortedNames = append(sortedNames, c.name) } } - - if getRIndex(sortedNames, c.name) == -1 { - sortedNames = append(sortedNames, c.name) - } } for _, cp := range cps { sortCallbackProcessor(cp) } - var funcs = []*func(scope *Scope){} - var sortedFuncs = []*func(scope *Scope){} + var sortedFuncs []*func(scope *Scope) for _, name := range sortedNames { - index := getRIndex(names, name) - if !cps[index].remove { + if index := getRIndex(allNames, name); !cps[index].remove { sortedFuncs = append(sortedFuncs, cps[index].processor) } } - for _, cp := range cps { - if sindex := getRIndex(sortedNames, cp.name); sindex == -1 { - if !cp.remove { - funcs = append(funcs, cp.processor) - } - } - } - - return append(sortedFuncs, funcs...) + return sortedFuncs } // reorder all registered processors, and reset CURD callbacks diff --git a/callback_delete.go b/callback_delete.go index 17b5cfb4..b3a77926 100644 --- a/callback_delete.go +++ b/callback_delete.go @@ -2,6 +2,7 @@ package gorm import "fmt" +// Define callbacks for deleting func init() { defaultCallback.Delete().Register("gorm:begin_transaction", beginTransactionCallback) defaultCallback.Delete().Register("gorm:before_delete", beforeDeleteCallback) @@ -10,12 +11,14 @@ func init() { defaultCallback.Delete().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback) } +// beforeDeleteCallback will invoke `BeforeDelete` method before deleting func beforeDeleteCallback(scope *Scope) { if !scope.HasError() { scope.CallMethod("BeforeDelete") } } +// deleteCallback used to delete data from database or set deleted_at to current time (when using with soft delete) func deleteCallback(scope *Scope) { if !scope.HasError() { if !scope.Search.Unscoped && scope.HasColumn("DeletedAt") { @@ -24,15 +27,14 @@ func deleteCallback(scope *Scope) { scope.QuotedTableName(), scope.AddToVars(NowFunc()), scope.CombinedConditionSql(), - )) + )).Exec() } else { - scope.Raw(fmt.Sprintf("DELETE FROM %v %v", scope.QuotedTableName(), scope.CombinedConditionSql())) + scope.Raw(fmt.Sprintf("DELETE FROM %v %v", scope.QuotedTableName(), scope.CombinedConditionSql())).Exec() } - - scope.Exec() } } +// afterDeleteCallback will invoke `AfterDelete` method after deleting func afterDeleteCallback(scope *Scope) { if !scope.HasError() { scope.CallMethod("AfterDelete") diff --git a/callback_query.go b/callback_query.go index f6fa0aa1..5d9fd82d 100644 --- a/callback_query.go +++ b/callback_query.go @@ -6,40 +6,42 @@ import ( "reflect" ) +// Define callbacks for querying func init() { defaultCallback.Query().Register("gorm:query", queryCallback) defaultCallback.Query().Register("gorm:after_query", afterQueryCallback) defaultCallback.Query().Register("gorm:preload", preloadCallback) } +// queryCallback used to query data from database func queryCallback(scope *Scope) { defer scope.trace(NowFunc()) var ( - isSlice bool - isPtr bool - destType reflect.Type + isSlice bool + isPtr bool + results = scope.IndirectValue() + resultType reflect.Type ) if orderBy, ok := scope.Get("gorm:order_by_primary_key"); ok { - if primaryKey := scope.PrimaryKey(); primaryKey != "" { - scope.Search.Order(fmt.Sprintf("%v.%v %v", scope.QuotedTableName(), scope.Quote(primaryKey), orderBy)) + if primaryField := scope.PrimaryField(); primaryField != nil { + scope.Search.Order(fmt.Sprintf("%v.%v %v", scope.QuotedTableName(), scope.Quote(primaryField.DBName), orderBy)) } } - var dest = scope.IndirectValue() if value, ok := scope.Get("gorm:query_destination"); ok { - dest = reflect.Indirect(reflect.ValueOf(value)) + results = reflect.Indirect(reflect.ValueOf(value)) } - if kind := dest.Kind(); kind == reflect.Slice { + if kind := results.Kind(); kind == reflect.Slice { isSlice = true - destType = dest.Type().Elem() - dest.Set(reflect.MakeSlice(dest.Type(), 0, 0)) + resultType = results.Type().Elem() + results.Set(reflect.MakeSlice(results.Type(), 0, 0)) - if destType.Kind() == reflect.Ptr { + if resultType.Kind() == reflect.Ptr { isPtr = true - destType = destType.Elem() + resultType = resultType.Elem() } } else if kind != reflect.Struct { scope.Err(errors.New("unsupported destination, should be slice or struct")) @@ -49,41 +51,38 @@ func queryCallback(scope *Scope) { scope.prepareQuerySql() if !scope.HasError() { - rows, err := scope.SqlDB().Query(scope.Sql, scope.SqlVars...) scope.db.RowsAffected = 0 + if rows, err := scope.SqlDB().Query(scope.Sql, scope.SqlVars...); scope.Err(err) == nil { + defer rows.Close() - if scope.Err(err) != nil { - return - } - defer rows.Close() + columns, _ := rows.Columns() + for rows.Next() { + scope.db.RowsAffected++ - columns, _ := rows.Columns() - for rows.Next() { - scope.db.RowsAffected++ + elem := results + if isSlice { + elem = reflect.New(resultType).Elem() + } - elem := dest - if isSlice { - elem = reflect.New(destType).Elem() - } + scope.scan(rows, columns, scope.New(elem.Addr().Interface()).Fields()) - fields := scope.New(elem.Addr().Interface()).Fields() - scope.scan(rows, columns, fields) - - if isSlice { - if isPtr { - dest.Set(reflect.Append(dest, elem.Addr())) - } else { - dest.Set(reflect.Append(dest, elem)) + if isSlice { + if isPtr { + results.Set(reflect.Append(results, elem.Addr())) + } else { + results.Set(reflect.Append(results, elem)) + } } } - } - if scope.db.RowsAffected == 0 && !isSlice { - scope.Err(RecordNotFound) + if scope.db.RowsAffected == 0 && !isSlice { + scope.Err(RecordNotFound) + } } } } +// afterQueryCallback will invoke `AfterFind` method after querying func afterQueryCallback(scope *Scope) { if !scope.HasError() { scope.CallMethod("AfterFind") diff --git a/callback_query_preload.go b/callback_query_preload.go index 5dc91de9..ff99fea9 100644 --- a/callback_query_preload.go +++ b/callback_query_preload.go @@ -7,6 +7,7 @@ import ( "strings" ) +// preloadCallback used to preload associations func preloadCallback(scope *Scope) { if scope.Search.preload == nil || scope.HasError() { return @@ -72,6 +73,7 @@ func preloadCallback(scope *Scope) { } } +// handleHasOnePreload used to preload has one associations func (scope *Scope) handleHasOnePreload(field *Field, conditions []interface{}) { relation := field.Relationship @@ -107,6 +109,7 @@ func (scope *Scope) handleHasOnePreload(field *Field, conditions []interface{}) } } +// handleHasManyPreload used to preload has many associations func (scope *Scope) handleHasManyPreload(field *Field, conditions []interface{}) { relation := field.Relationship @@ -144,6 +147,7 @@ func (scope *Scope) handleHasManyPreload(field *Field, conditions []interface{}) } } +// handleBelongsToPreload used to preload belongs to associations func (scope *Scope) handleBelongsToPreload(field *Field, conditions []interface{}) { relation := field.Relationship @@ -179,6 +183,7 @@ func (scope *Scope) handleBelongsToPreload(field *Field, conditions []interface{ } } +// handleManyToManyPreload used to preload many to many associations func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface{}) { var ( relation = field.Relationship From 84f34c0c62b8df3b90f5fe1572a84943154ff14d Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 17 Jan 2016 21:01:18 +0800 Subject: [PATCH 29/83] Move logger images to doc --- README.md | 2 +- {images => doc}/logger.png | Bin 2 files changed, 1 insertion(+), 1 deletion(-) rename {images => doc}/logger.png (100%) diff --git a/README.md b/README.md index fa710802..9a671386 100644 --- a/README.md +++ b/README.md @@ -1191,7 +1191,7 @@ db.LogMode(false) db.Debug().Where("name = ?", "jinzhu").First(&User{}) ``` -![logger](https://raw.github.com/jinzhu/gorm/master/images/logger.png) +![logger](https://raw.github.com/jinzhu/gorm/master/doc/logger.png) ### Customize Logger diff --git a/images/logger.png b/doc/logger.png similarity index 100% rename from images/logger.png rename to doc/logger.png From 19e9bd29e37d2d1556d8aeba2c3f44e8d25a3421 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 17 Jan 2016 21:35:32 +0800 Subject: [PATCH 30/83] Refactor Scope CallMethod --- scope.go | 73 +++++++++++++++++++++++++++----------------------------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/scope.go b/scope.go index b0c2432c..09da8e11 100644 --- a/scope.go +++ b/scope.go @@ -182,50 +182,47 @@ func (scope *Scope) SetColumn(column interface{}, value interface{}) error { return errors.New("could not convert column to field") } -func (scope *Scope) CallMethod(name string) { +func (scope *Scope) callMethod(methodName string, reflectValue reflect.Value) { + if reflectValue.CanAddr() { + reflectValue = reflectValue.Addr() + } + + if methodValue := reflectValue.MethodByName(methodName); methodValue.IsValid() { + switch method := methodValue.Interface().(type) { + case func(): + method() + case func(*Scope): + method(scope) + case func(*DB): + newDB := scope.NewDB() + method(newDB) + scope.Err(newDB.Error) + case func() error: + scope.Err(method()) + case func(*Scope) error: + scope.Err(method(scope)) + case func(*DB) error: + newDB := scope.NewDB() + scope.Err(method(newDB)) + scope.Err(newDB.Error) + default: + scope.Err(fmt.Errorf("unsupported function %v", methodName)) + } + } +} + +// CallMethod call scope value's method, if it is a slice, will call value's method one by one +func (scope *Scope) CallMethod(methodName string) { if scope.Value == nil { return } - call := func(value interface{}) { - if fm := reflect.ValueOf(value).MethodByName(name); fm.IsValid() { - switch f := fm.Interface().(type) { - case func(): - f() - case func(s *Scope): - f(scope) - case func(s *DB): - newDB := scope.NewDB() - f(newDB) - scope.Err(newDB.Error) - case func() error: - scope.Err(f()) - case func(s *Scope) error: - scope.Err(f(scope)) - case func(s *DB) error: - newDB := scope.NewDB() - scope.Err(f(newDB)) - scope.Err(newDB.Error) - default: - scope.Err(fmt.Errorf("unsupported function %v", name)) - } - } - } - - if values := scope.IndirectValue(); values.Kind() == reflect.Slice { - for i := 0; i < values.Len(); i++ { - value := values.Index(i).Addr().Interface() - if values.Index(i).Kind() == reflect.Ptr { - value = values.Index(i).Interface() - } - call(value) + if indirectScopeValue := scope.IndirectValue(); indirectScopeValue.Kind() == reflect.Slice { + for i := 0; i < indirectScopeValue.Len(); i++ { + scope.callMethod(methodName, indirectScopeValue.Index(i)) } } else { - if scope.IndirectValue().CanAddr() { - call(scope.IndirectValue().Addr().Interface()) - } else { - call(scope.IndirectValue().Interface()) - } + scope.callMethod(methodName, indirectScopeValue) } } From e159ca1914e74d9fdbbac34274472a65ea3576f2 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 18 Jan 2016 20:32:52 +0800 Subject: [PATCH 31/83] Refactor dialect --- dialect.go | 16 ++++++------ dialect_common.go | 60 +++++++++++++++++++++++++-------------------- dialect_mssql.go | 33 +++++++++++++++++-------- dialect_mysql.go | 14 +++++------ dialect_postgres.go | 56 ++++++++++++++++++++---------------------- dialect_sqlite3.go | 20 +++++++-------- main.go | 8 +++--- main_private.go | 2 +- scope.go | 2 +- scope_private.go | 39 +++-------------------------- search.go | 12 ++++----- 11 files changed, 124 insertions(+), 138 deletions(-) diff --git a/dialect.go b/dialect.go index aa23e31b..1fa61925 100644 --- a/dialect.go +++ b/dialect.go @@ -7,17 +7,19 @@ import ( type Dialect interface { BinVar(i int) string - SupportLastInsertId() bool - HasTop() bool - SqlTag(value reflect.Value, size int, autoIncrease bool) string - ReturningStr(tableName, key string) string - SelectFromDummyTable() string Quote(key string) string - HasTable(scope *Scope, tableName string) bool - HasColumn(scope *Scope, tableName string, columnName string) bool + SqlTag(value reflect.Value, size int, autoIncrease bool) string + HasIndex(scope *Scope, tableName string, indexName string) bool RemoveIndex(scope *Scope, indexName string) + HasTable(scope *Scope, tableName string) bool + HasColumn(scope *Scope, tableName string, columnName string) bool CurrentDatabase(scope *Scope) string + + ReturningStr(tableName, key string) string + LimitAndOffsetSQL(limit, offset int) string + SelectFromDummyTable() string + SupportLastInsertId() bool } func NewDialect(driver string) Dialect { diff --git a/dialect_common.go b/dialect_common.go index 7f08b04f..ade7c068 100644 --- a/dialect_common.go +++ b/dialect_common.go @@ -12,12 +12,8 @@ func (commonDialect) BinVar(i int) string { return "$$" // ? } -func (commonDialect) SupportLastInsertId() bool { - return true -} - -func (commonDialect) HasTop() bool { - return false +func (commonDialect) Quote(key string) string { + return fmt.Sprintf(`"%s"`, key) } func (commonDialect) SqlTag(value reflect.Value, size int, autoIncrease bool) string { @@ -56,16 +52,17 @@ func (commonDialect) SqlTag(value reflect.Value, size int, autoIncrease bool) st panic(fmt.Sprintf("invalid sql type %s (%s) for commonDialect", value.Type().Name(), value.Kind().String())) } -func (commonDialect) ReturningStr(tableName, key string) string { - return "" +func (c commonDialect) HasIndex(scope *Scope, tableName string, indexName string) bool { + var ( + count int + databaseName = c.CurrentDatabase(scope) + ) + c.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE table_schema = ? AND table_name = ? AND index_name = ?", databaseName, tableName, indexName) + return count > 0 } -func (commonDialect) SelectFromDummyTable() string { - return "" -} - -func (commonDialect) Quote(key string) string { - return fmt.Sprintf(`"%s"`, key) +func (commonDialect) RemoveIndex(scope *Scope, indexName string) { + scope.Err(scope.NewDB().Exec(fmt.Sprintf("DROP INDEX %v ON %v", indexName, scope.QuotedTableName())).Error) } func (c commonDialect) HasTable(scope *Scope, tableName string) bool { @@ -86,19 +83,6 @@ func (c commonDialect) HasColumn(scope *Scope, tableName string, columnName stri return count > 0 } -func (c commonDialect) HasIndex(scope *Scope, tableName string, indexName string) bool { - var ( - count int - databaseName = c.CurrentDatabase(scope) - ) - c.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE table_schema = ? AND table_name = ? AND index_name = ?", databaseName, tableName, indexName) - return count > 0 -} - -func (commonDialect) RemoveIndex(scope *Scope, indexName string) { - scope.Err(scope.NewDB().Exec(fmt.Sprintf("DROP INDEX %v ON %v", indexName, scope.QuotedTableName())).Error) -} - // RawScanInt scans the first column of the first row into the `scan' int pointer. // This function captures raw query errors and propagates them to the original scope. func (commonDialect) RawScanInt(scope *Scope, scanPtr *int, query string, args ...interface{}) { @@ -115,3 +99,25 @@ func (commonDialect) CurrentDatabase(scope *Scope) (name string) { scope.Err(scope.NewDB().Raw("SELECT DATABASE()").Row().Scan(&name)) return } + +func (commonDialect) ReturningStr(tableName, key string) string { + return "" +} + +func (commonDialect) LimitAndOffsetSQL(limit, offset int) (sql string) { + if limit >= 0 { + sql += fmt.Sprintf(" LIMIT %d", limit) + } + if offset >= 0 { + sql += fmt.Sprintf(" OFFSET %d", offset) + } + return +} + +func (commonDialect) SelectFromDummyTable() string { + return "" +} + +func (commonDialect) SupportLastInsertId() bool { + return true +} diff --git a/dialect_mssql.go b/dialect_mssql.go index a9bd1e52..82fba7d1 100644 --- a/dialect_mssql.go +++ b/dialect_mssql.go @@ -10,10 +10,6 @@ type mssql struct { commonDialect } -func (mssql) HasTop() bool { - return true -} - func (mssql) SqlTag(value reflect.Value, size int, autoIncrease bool) string { switch value.Kind() { case reflect.Bool: @@ -50,6 +46,12 @@ func (mssql) SqlTag(value reflect.Value, size int, autoIncrease bool) string { panic(fmt.Sprintf("invalid sql type %s (%s) for mssql", value.Type().Name(), value.Kind().String())) } +func (s mssql) HasIndex(scope *Scope, tableName string, indexName string) bool { + var count int + s.RawScanInt(scope, &count, "SELECT count(*) FROM sys.indexes WHERE name=? AND object_id=OBJECT_ID(?)", indexName, tableName) + return count > 0 +} + func (s mssql) HasTable(scope *Scope, tableName string) bool { var ( count int @@ -68,13 +70,24 @@ func (s mssql) HasColumn(scope *Scope, tableName string, columnName string) bool return count > 0 } -func (s mssql) HasIndex(scope *Scope, tableName string, indexName string) bool { - var count int - s.RawScanInt(scope, &count, "SELECT count(*) FROM sys.indexes WHERE name=? AND object_id=OBJECT_ID(?)", indexName, tableName) - return count > 0 -} - func (s mssql) CurrentDatabase(scope *Scope) (name string) { s.RawScanString(scope, &name, "SELECT DB_NAME() AS [Current Database]") return } + +func (mssql) LimitAndOffsetSQL(limit, offset int) (sql string) { + if limit < 0 && offset < 0 { + return + } + + if offset < 0 { + offset = 0 + } + + sql += fmt.Sprintf(" OFFSET %d ROWS", offset) + + if limit >= 0 { + sql += fmt.Sprintf(" FETCH NEXT %d ROWS ONLY", limit) + } + return +} diff --git a/dialect_mysql.go b/dialect_mysql.go index 9e1d56d3..b6f9a22b 100644 --- a/dialect_mysql.go +++ b/dialect_mysql.go @@ -10,6 +10,10 @@ type mysql struct { commonDialect } +func (mysql) Quote(key string) string { + return fmt.Sprintf("`%s`", key) +} + func (mysql) SqlTag(value reflect.Value, size int, autoIncrease bool) string { switch value.Kind() { case reflect.Bool: @@ -56,15 +60,11 @@ func (mysql) SqlTag(value reflect.Value, size int, autoIncrease bool) string { panic(fmt.Sprintf("invalid sql type %s (%s) for mysql", value.Type().Name(), value.Kind().String())) } -func (mysql) Quote(key string) string { - return fmt.Sprintf("`%s`", key) +func (s mysql) CurrentDatabase(scope *Scope) (name string) { + s.RawScanString(scope, &name, "SELECT DATABASE()") + return } func (mysql) SelectFromDummyTable() string { return "FROM DUAL" } - -func (s mysql) CurrentDatabase(scope *Scope) (name string) { - s.RawScanString(scope, &name, "SELECT DATABASE()") - return -} diff --git a/dialect_postgres.go b/dialect_postgres.go index 3b083dfa..0b16816c 100644 --- a/dialect_postgres.go +++ b/dialect_postgres.go @@ -19,10 +19,6 @@ func (postgres) BinVar(i int) string { return fmt.Sprintf("$%v", i) } -func (postgres) SupportLastInsertId() bool { - return false -} - func (postgres) SqlTag(value reflect.Value, size int, autoIncrease bool) string { switch value.Kind() { case reflect.Bool: @@ -62,23 +58,14 @@ func (postgres) SqlTag(value reflect.Value, size int, autoIncrease bool) string panic(fmt.Sprintf("invalid sql type %s (%s) for postgres", value.Type().Name(), value.Kind().String())) } -var byteType = reflect.TypeOf(uint8(0)) - -func isByteArrayOrSlice(value reflect.Value) bool { - return (value.Kind() == reflect.Array || value.Kind() == reflect.Slice) && value.Type().Elem() == byteType +func (s postgres) HasIndex(scope *Scope, tableName string, indexName string) bool { + var count int + s.RawScanInt(scope, &count, "SELECT count(*) FROM pg_indexes WHERE tablename = ? AND indexname = ?", tableName, indexName) + return count > 0 } -func isUUID(value reflect.Value) bool { - if value.Kind() != reflect.Array || value.Type().Len() != 16 { - return false - } - typename := value.Type().Name() - lower := strings.ToLower(typename) - return "uuid" == lower || "guid" == lower -} - -func (s postgres) ReturningStr(tableName, key string) string { - return fmt.Sprintf("RETURNING %v.%v", tableName, key) +func (postgres) RemoveIndex(scope *Scope, indexName string) { + scope.Err(scope.NewDB().Exec(fmt.Sprintf("DROP INDEX %v", indexName)).Error) } func (s postgres) HasTable(scope *Scope, tableName string) bool { @@ -93,21 +80,19 @@ func (s postgres) HasColumn(scope *Scope, tableName string, columnName string) b return count > 0 } -func (postgres) RemoveIndex(scope *Scope, indexName string) { - scope.Err(scope.NewDB().Exec(fmt.Sprintf("DROP INDEX %v", indexName)).Error) -} - -func (s postgres) HasIndex(scope *Scope, tableName string, indexName string) bool { - var count int - s.RawScanInt(scope, &count, "SELECT count(*) FROM pg_indexes WHERE tablename = ? AND indexname = ?", tableName, indexName) - return count > 0 -} - func (s postgres) CurrentDatabase(scope *Scope) (name string) { s.RawScanString(scope, &name, "SELECT CURRENT_DATABASE()") return } +func (s postgres) ReturningStr(tableName, key string) string { + return fmt.Sprintf("RETURNING %v.%v", tableName, key) +} + +func (postgres) SupportLastInsertId() bool { + return false +} + var hstoreType = reflect.TypeOf(Hstore{}) type Hstore map[string]*string @@ -152,3 +137,16 @@ func (h *Hstore) Scan(value interface{}) error { return nil } + +func isByteArrayOrSlice(value reflect.Value) bool { + return (value.Kind() == reflect.Array || value.Kind() == reflect.Slice) && value.Type().Elem() == reflect.TypeOf(uint8(0)) +} + +func isUUID(value reflect.Value) bool { + if value.Kind() != reflect.Array || value.Type().Len() != 16 { + return false + } + typename := value.Type().Name() + lower := strings.ToLower(typename) + return "uuid" == lower || "guid" == lower +} diff --git a/dialect_sqlite3.go b/dialect_sqlite3.go index d052d2c1..82546dbb 100644 --- a/dialect_sqlite3.go +++ b/dialect_sqlite3.go @@ -43,6 +43,16 @@ func (sqlite3) SqlTag(value reflect.Value, size int, autoIncrease bool) string { panic(fmt.Sprintf("invalid sql type %s (%s) for sqlite3", value.Type().Name(), value.Kind().String())) } +func (s sqlite3) HasIndex(scope *Scope, tableName string, indexName string) bool { + var count int + s.RawScanInt(scope, &count, fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE tbl_name = ? AND sql LIKE '%%INDEX %v ON%%'", indexName), tableName) + return count > 0 +} + +func (sqlite3) RemoveIndex(scope *Scope, indexName string) { + scope.Err(scope.NewDB().Exec(fmt.Sprintf("DROP INDEX %v", indexName)).Error) +} + func (s sqlite3) HasTable(scope *Scope, tableName string) bool { var count int s.RawScanInt(scope, &count, "SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?", tableName) @@ -55,16 +65,6 @@ func (s sqlite3) HasColumn(scope *Scope, tableName string, columnName string) bo return count > 0 } -func (s sqlite3) HasIndex(scope *Scope, tableName string, indexName string) bool { - var count int - s.RawScanInt(scope, &count, fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE tbl_name = ? AND sql LIKE '%%INDEX %v ON%%'", indexName), tableName) - return count > 0 -} - -func (sqlite3) RemoveIndex(scope *Scope, indexName string) { - scope.Err(scope.NewDB().Exec(fmt.Sprintf("DROP INDEX %v", indexName)).Error) -} - func (sqlite3) CurrentDatabase(scope *Scope) (name string) { var ( ifaces = make([]interface{}, 3) diff --git a/main.go b/main.go index a56f282f..461329fa 100644 --- a/main.go +++ b/main.go @@ -146,12 +146,12 @@ func (s *DB) Not(query interface{}, args ...interface{}) *DB { return s.clone().search.Not(query, args...).db } -func (s *DB) Limit(value interface{}) *DB { - return s.clone().search.Limit(value).db +func (s *DB) Limit(limit int) *DB { + return s.clone().search.Limit(limit).db } -func (s *DB) Offset(value interface{}) *DB { - return s.clone().search.Offset(value).db +func (s *DB) Offset(offset int) *DB { + return s.clone().search.Offset(offset).db } func (s *DB) Order(value string, reorder ...bool) *DB { diff --git a/main_private.go b/main_private.go index bd097ce0..a6e5a6a9 100644 --- a/main_private.go +++ b/main_private.go @@ -10,7 +10,7 @@ func (s *DB) clone() *DB { } if s.search == nil { - db.search = &search{} + db.search = &search{limit: -1, offset: -1} } else { db.search = s.search.clone() } diff --git a/scope.go b/scope.go index 1608a99b..8ee4bdd5 100644 --- a/scope.go +++ b/scope.go @@ -272,7 +272,7 @@ func (scope *Scope) QuotedTableName() (name string) { // CombinedConditionSql get combined condition sql func (scope *Scope) CombinedConditionSql() string { return scope.joinsSql() + scope.whereSql() + scope.groupSql() + - scope.havingSql() + scope.orderSql() + scope.limitSql() + scope.offsetSql() + scope.havingSql() + scope.orderSql() + scope.limitAndOffsetSql() } // FieldByName find gorm.Field with name and db name diff --git a/scope_private.go b/scope_private.go index ef16cf93..dc1676e8 100644 --- a/scope_private.go +++ b/scope_private.go @@ -245,41 +245,8 @@ func (scope *Scope) orderSql() string { return " ORDER BY " + strings.Join(scope.Search.orders, ",") } -func (scope *Scope) limitSql() string { - if !scope.Dialect().HasTop() { - if len(scope.Search.limit) == 0 { - return "" - } - return " LIMIT " + scope.Search.limit - } - - return "" -} - -func (scope *Scope) topSql() string { - if scope.Dialect().HasTop() && len(scope.Search.offset) == 0 { - if len(scope.Search.limit) == 0 { - return "" - } - return " TOP(" + scope.Search.limit + ")" - } - - return "" -} - -func (scope *Scope) offsetSql() string { - if len(scope.Search.offset) == 0 { - return "" - } - - if scope.Dialect().HasTop() { - sql := " OFFSET " + scope.Search.offset + " ROW " - if len(scope.Search.limit) > 0 { - sql += "FETCH NEXT " + scope.Search.limit + " ROWS ONLY" - } - return sql - } - return " OFFSET " + scope.Search.offset +func (scope *Scope) limitAndOffsetSql() string { + return scope.Dialect().LimitAndOffsetSQL(scope.Search.limit, scope.Search.offset) } func (scope *Scope) groupSql() string { @@ -318,7 +285,7 @@ func (scope *Scope) prepareQuerySql() { if scope.Search.raw { scope.Raw(strings.TrimSuffix(strings.TrimPrefix(scope.CombinedConditionSql(), " WHERE ("), ")")) } else { - scope.Raw(fmt.Sprintf("SELECT %v %v FROM %v %v", scope.topSql(), scope.selectSql(), scope.QuotedTableName(), scope.CombinedConditionSql())) + scope.Raw(fmt.Sprintf("SELECT %v FROM %v %v", scope.selectSql(), scope.QuotedTableName(), scope.CombinedConditionSql())) } return } diff --git a/search.go b/search.go index 166b9a86..c6d070f0 100644 --- a/search.go +++ b/search.go @@ -15,8 +15,8 @@ type search struct { orders []string joins string preload []searchPreload - offset string - limit string + offset int + limit int group string tableName string raw bool @@ -82,13 +82,13 @@ func (s *search) Omit(columns ...string) *search { return s } -func (s *search) Limit(value interface{}) *search { - s.limit = s.getInterfaceAsSql(value) +func (s *search) Limit(limit int) *search { + s.limit = limit return s } -func (s *search) Offset(value interface{}) *search { - s.offset = s.getInterfaceAsSql(value) +func (s *search) Offset(offset int) *search { + s.offset = offset return s } From d92c5db9e75d239af6ea016d098c90a84b6cce1d Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 19 Jan 2016 11:53:53 +0800 Subject: [PATCH 32/83] Refactor dialect --- callback_create.go | 21 ++++++++++++--------- dialect.go | 19 ++++++++++++++----- dialect_common.go | 20 ++++++++------------ dialect_mssql.go | 8 ++++---- dialect_mysql.go | 4 ++-- dialect_postgres.go | 8 ++++---- dialect_sqlite3.go | 4 ++-- main.go | 8 -------- model_struct.go | 2 +- query_test.go | 11 ----------- scope.go | 2 +- scope_private.go | 4 ++-- 12 files changed, 50 insertions(+), 61 deletions(-) diff --git a/callback_create.go b/callback_create.go index b8725363..0ba3feac 100644 --- a/callback_create.go +++ b/callback_create.go @@ -71,29 +71,32 @@ func createCallback(scope *Scope) { } } - returningKey := "*" - primaryField := scope.PrimaryField() + var ( + returningColumn = "*" + quotedTableName = scope.QuotedTableName() + primaryField = scope.PrimaryField() + ) + if primaryField != nil { - returningKey = scope.Quote(primaryField.DBName) + returningColumn = scope.Quote(primaryField.DBName) } + lastInsertIdReturningSuffix := scope.Dialect().LastInsertIdReturningSuffix(quotedTableName, returningColumn) + if len(columns) == 0 { - scope.Raw(fmt.Sprintf("INSERT INTO %v DEFAULT VALUES %v", - scope.QuotedTableName(), - scope.Dialect().ReturningStr(scope.QuotedTableName(), returningKey), - )) + scope.Raw(fmt.Sprintf("INSERT INTO %v DEFAULT VALUES %v", quotedTableName, lastInsertIdReturningSuffix)) } else { scope.Raw(fmt.Sprintf( "INSERT INTO %v (%v) VALUES (%v) %v", scope.QuotedTableName(), strings.Join(columns, ","), strings.Join(placeholders, ","), - scope.Dialect().ReturningStr(scope.QuotedTableName(), returningKey), + lastInsertIdReturningSuffix, )) } // execute create sql - if scope.Dialect().SupportLastInsertId() || primaryField == nil { + if lastInsertIdReturningSuffix == "" || primaryField == nil { if result, err := scope.SqlDB().Exec(scope.Sql, scope.SqlVars...); scope.Err(err) == nil { // set rows affected count scope.db.RowsAffected, _ = result.RowsAffected() diff --git a/dialect.go b/dialect.go index 1fa61925..72b6b2aa 100644 --- a/dialect.go +++ b/dialect.go @@ -5,21 +5,30 @@ import ( "reflect" ) +// Dialect interface contains behaviors that differ across SQL database type Dialect interface { - BinVar(i int) string + // BindVar return the placeholder for actual values in SQL statements, in many dbs it is "?", Postgres using $1 + BindVar(i int) string + // Quote quotes field name to avoid SQL parsing exceptions by using a reserved word as a field name Quote(key string) string - SqlTag(value reflect.Value, size int, autoIncrease bool) string + // DataTypeOf return data's sql type + DataTypeOf(value reflect.Value, size int, autoIncrease bool) string + // HasIndex check has index or not HasIndex(scope *Scope, tableName string, indexName string) bool + // RemoveIndex remove index RemoveIndex(scope *Scope, indexName string) + // HasTable check has table or not HasTable(scope *Scope, tableName string) bool + // HasColumn check has column or not HasColumn(scope *Scope, tableName string, columnName string) bool - CurrentDatabase(scope *Scope) string - ReturningStr(tableName, key string) string + // LimitAndOffsetSQL return generate SQL with limit and offset, as mssql has special case LimitAndOffsetSQL(limit, offset int) string + // SelectFromDummyTable return select values, for most dbs, `SELECT values` just works, mysql needs `SELECT value FROM DUAL` SelectFromDummyTable() string - SupportLastInsertId() bool + // LastInsertIdReturningSuffix most dbs support LastInsertId, but postgres needs to use `RETURNING` + LastInsertIdReturningSuffix(tableName, columnName string) string } func NewDialect(driver string) Dialect { diff --git a/dialect_common.go b/dialect_common.go index ade7c068..efc8d642 100644 --- a/dialect_common.go +++ b/dialect_common.go @@ -8,7 +8,7 @@ import ( type commonDialect struct{} -func (commonDialect) BinVar(i int) string { +func (commonDialect) BindVar(i int) string { return "$$" // ? } @@ -16,7 +16,7 @@ func (commonDialect) Quote(key string) string { return fmt.Sprintf(`"%s"`, key) } -func (commonDialect) SqlTag(value reflect.Value, size int, autoIncrease bool) string { +func (commonDialect) DataTypeOf(value reflect.Value, size int, autoIncrease bool) string { switch value.Kind() { case reflect.Bool: return "BOOLEAN" @@ -55,7 +55,7 @@ func (commonDialect) SqlTag(value reflect.Value, size int, autoIncrease bool) st func (c commonDialect) HasIndex(scope *Scope, tableName string, indexName string) bool { var ( count int - databaseName = c.CurrentDatabase(scope) + databaseName = c.currentDatabase(scope) ) c.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE table_schema = ? AND table_name = ? AND index_name = ?", databaseName, tableName, indexName) return count > 0 @@ -68,7 +68,7 @@ func (commonDialect) RemoveIndex(scope *Scope, indexName string) { func (c commonDialect) HasTable(scope *Scope, tableName string) bool { var ( count int - databaseName = c.CurrentDatabase(scope) + databaseName = c.currentDatabase(scope) ) c.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = ? AND table_name = ?", databaseName, tableName) return count > 0 @@ -77,7 +77,7 @@ func (c commonDialect) HasTable(scope *Scope, tableName string) bool { func (c commonDialect) HasColumn(scope *Scope, tableName string, columnName string) bool { var ( count int - databaseName = c.CurrentDatabase(scope) + databaseName = c.currentDatabase(scope) ) c.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = ? AND table_name = ? AND column_name = ?", databaseName, tableName, columnName) return count > 0 @@ -95,15 +95,11 @@ func (commonDialect) RawScanString(scope *Scope, scanPtr *string, query string, scope.Err(scope.NewDB().Raw(query, args...).Row().Scan(scanPtr)) } -func (commonDialect) CurrentDatabase(scope *Scope) (name string) { +func (commonDialect) currentDatabase(scope *Scope) (name string) { scope.Err(scope.NewDB().Raw("SELECT DATABASE()").Row().Scan(&name)) return } -func (commonDialect) ReturningStr(tableName, key string) string { - return "" -} - func (commonDialect) LimitAndOffsetSQL(limit, offset int) (sql string) { if limit >= 0 { sql += fmt.Sprintf(" LIMIT %d", limit) @@ -118,6 +114,6 @@ func (commonDialect) SelectFromDummyTable() string { return "" } -func (commonDialect) SupportLastInsertId() bool { - return true +func (commonDialect) LastInsertIdReturningSuffix(tableName, columnName string) string { + return "" } diff --git a/dialect_mssql.go b/dialect_mssql.go index 82fba7d1..c3e21c97 100644 --- a/dialect_mssql.go +++ b/dialect_mssql.go @@ -10,7 +10,7 @@ type mssql struct { commonDialect } -func (mssql) SqlTag(value reflect.Value, size int, autoIncrease bool) string { +func (mssql) DataTypeOf(value reflect.Value, size int, autoIncrease bool) string { switch value.Kind() { case reflect.Bool: return "bit" @@ -55,7 +55,7 @@ func (s mssql) HasIndex(scope *Scope, tableName string, indexName string) bool { func (s mssql) HasTable(scope *Scope, tableName string) bool { var ( count int - databaseName = s.CurrentDatabase(scope) + databaseName = s.currentDatabase(scope) ) s.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.tables WHERE table_name = ? AND table_catalog = ?", tableName, databaseName) return count > 0 @@ -64,13 +64,13 @@ func (s mssql) HasTable(scope *Scope, tableName string) bool { func (s mssql) HasColumn(scope *Scope, tableName string, columnName string) bool { var ( count int - databaseName = s.CurrentDatabase(scope) + databaseName = s.currentDatabase(scope) ) s.RawScanInt(scope, &count, "SELECT count(*) FROM information_schema.columns WHERE table_catalog = ? AND table_name = ? AND column_name = ?", databaseName, tableName, columnName) return count > 0 } -func (s mssql) CurrentDatabase(scope *Scope) (name string) { +func (s mssql) currentDatabase(scope *Scope) (name string) { s.RawScanString(scope, &name, "SELECT DB_NAME() AS [Current Database]") return } diff --git a/dialect_mysql.go b/dialect_mysql.go index b6f9a22b..e334c7a4 100644 --- a/dialect_mysql.go +++ b/dialect_mysql.go @@ -14,7 +14,7 @@ func (mysql) Quote(key string) string { return fmt.Sprintf("`%s`", key) } -func (mysql) SqlTag(value reflect.Value, size int, autoIncrease bool) string { +func (mysql) DataTypeOf(value reflect.Value, size int, autoIncrease bool) string { switch value.Kind() { case reflect.Bool: return "boolean" @@ -60,7 +60,7 @@ func (mysql) SqlTag(value reflect.Value, size int, autoIncrease bool) string { panic(fmt.Sprintf("invalid sql type %s (%s) for mysql", value.Type().Name(), value.Kind().String())) } -func (s mysql) CurrentDatabase(scope *Scope) (name string) { +func (s mysql) currentDatabase(scope *Scope) (name string) { s.RawScanString(scope, &name, "SELECT DATABASE()") return } diff --git a/dialect_postgres.go b/dialect_postgres.go index 0b16816c..c4742aec 100644 --- a/dialect_postgres.go +++ b/dialect_postgres.go @@ -15,11 +15,11 @@ type postgres struct { commonDialect } -func (postgres) BinVar(i int) string { +func (postgres) BindVar(i int) string { return fmt.Sprintf("$%v", i) } -func (postgres) SqlTag(value reflect.Value, size int, autoIncrease bool) string { +func (postgres) DataTypeOf(value reflect.Value, size int, autoIncrease bool) string { switch value.Kind() { case reflect.Bool: return "boolean" @@ -80,12 +80,12 @@ func (s postgres) HasColumn(scope *Scope, tableName string, columnName string) b return count > 0 } -func (s postgres) CurrentDatabase(scope *Scope) (name string) { +func (s postgres) currentDatabase(scope *Scope) (name string) { s.RawScanString(scope, &name, "SELECT CURRENT_DATABASE()") return } -func (s postgres) ReturningStr(tableName, key string) string { +func (s postgres) LastInsertIdReturningSuffix(tableName, key string) string { return fmt.Sprintf("RETURNING %v.%v", tableName, key) } diff --git a/dialect_sqlite3.go b/dialect_sqlite3.go index 82546dbb..e1e35bf7 100644 --- a/dialect_sqlite3.go +++ b/dialect_sqlite3.go @@ -10,7 +10,7 @@ type sqlite3 struct { commonDialect } -func (sqlite3) SqlTag(value reflect.Value, size int, autoIncrease bool) string { +func (sqlite3) DataTypeOf(value reflect.Value, size int, autoIncrease bool) string { switch value.Kind() { case reflect.Bool: return "bool" @@ -65,7 +65,7 @@ func (s sqlite3) HasColumn(scope *Scope, tableName string, columnName string) bo return count > 0 } -func (sqlite3) CurrentDatabase(scope *Scope) (name string) { +func (sqlite3) currentDatabase(scope *Scope) (name string) { var ( ifaces = make([]interface{}, 3) pointers = make([]*string, 3) diff --git a/main.go b/main.go index 461329fa..10c1b9be 100644 --- a/main.go +++ b/main.go @@ -453,14 +453,6 @@ func (s *DB) RemoveIndex(indexName string) *DB { return scope.db } -func (s *DB) CurrentDatabase() string { - var ( - scope = s.clone().NewScope(s.Value) - name = s.dialect.CurrentDatabase(scope) - ) - return name -} - // AddForeignKey Add foreign key to the given scope // Example: db.Model(&User{}).AddForeignKey("city_id", "cities(id)", "RESTRICT", "RESTRICT") func (s *DB) AddForeignKey(field string, dest string, onDelete string, onUpdate string) *DB { diff --git a/model_struct.go b/model_struct.go index b47f8534..c81dcd88 100644 --- a/model_struct.go +++ b/model_struct.go @@ -555,7 +555,7 @@ func (scope *Scope) generateSqlTag(field *StructField) string { autoIncrease = false } - sqlType = scope.Dialect().SqlTag(reflectValue, size, autoIncrease) + sqlType = scope.Dialect().DataTypeOf(reflectValue, size, autoIncrease) } if strings.TrimSpace(additionalType) == "" { diff --git a/query_test.go b/query_test.go index a7d5bc0e..b762dee5 100644 --- a/query_test.go +++ b/query_test.go @@ -621,14 +621,3 @@ func TestSelectWithArrayInput(t *testing.T) { t.Errorf("Should have selected both age and name") } } - -func TestCurrentDatabase(t *testing.T) { - databaseName := DB.CurrentDatabase() - if err := DB.Error; err != nil { - t.Errorf("Problem getting current db name: %s", err) - } - if databaseName == "" { - t.Errorf("Current db name returned empty; this should never happen!") - } - t.Logf("Got current db name: %v", databaseName) -} diff --git a/scope.go b/scope.go index 8ee4bdd5..6d9303ec 100644 --- a/scope.go +++ b/scope.go @@ -229,7 +229,7 @@ func (scope *Scope) AddToVars(value interface{}) string { } scope.SqlVars = append(scope.SqlVars, value) - return scope.Dialect().BinVar(len(scope.SqlVars)) + return scope.Dialect().BindVar(len(scope.SqlVars)) } type tabler interface { diff --git a/scope_private.go b/scope_private.go index dc1676e8..d5d384af 100644 --- a/scope_private.go +++ b/scope_private.go @@ -518,7 +518,7 @@ func (scope *Scope) createJoinTable(field *StructField) { value := reflect.Indirect(reflect.New(field.Struct.Type)) primaryKeySqlType := field.TagSettings["TYPE"] if primaryKeySqlType == "" { - primaryKeySqlType = scope.Dialect().SqlTag(value, 255, false) + primaryKeySqlType = scope.Dialect().DataTypeOf(value, 255, false) } sqlTypes = append(sqlTypes, scope.Quote(relationship.ForeignDBNames[idx])+" "+primaryKeySqlType) primaryKeys = append(primaryKeys, scope.Quote(relationship.ForeignDBNames[idx])) @@ -530,7 +530,7 @@ func (scope *Scope) createJoinTable(field *StructField) { value := reflect.Indirect(reflect.New(field.Struct.Type)) primaryKeySqlType := field.TagSettings["TYPE"] if primaryKeySqlType == "" { - primaryKeySqlType = scope.Dialect().SqlTag(value, 255, false) + primaryKeySqlType = scope.Dialect().DataTypeOf(value, 255, false) } sqlTypes = append(sqlTypes, scope.Quote(relationship.AssociationForeignDBNames[idx])+" "+primaryKeySqlType) primaryKeys = append(primaryKeys, scope.Quote(relationship.AssociationForeignDBNames[idx])) From 2dfd76d22bd75122110dc23225b31b685e6a769f Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 19 Jan 2016 20:58:38 +0800 Subject: [PATCH 33/83] Refactor DataTypeOf --- dialect.go | 2 +- dialect_common.go | 20 +++++++++++++------- dialect_mssql.go | 20 +++++++++++++------- dialect_mysql.go | 24 +++++++++++++++--------- dialect_postgres.go | 24 +++++++++++++++--------- dialect_sqlite3.go | 20 +++++++++++++------- model_struct.go | 17 +---------------- scope_private.go | 12 ++---------- 8 files changed, 73 insertions(+), 66 deletions(-) diff --git a/dialect.go b/dialect.go index 72b6b2aa..dd3c032e 100644 --- a/dialect.go +++ b/dialect.go @@ -12,7 +12,7 @@ type Dialect interface { // Quote quotes field name to avoid SQL parsing exceptions by using a reserved word as a field name Quote(key string) string // DataTypeOf return data's sql type - DataTypeOf(value reflect.Value, size int, autoIncrease bool) string + DataTypeOf(value reflect.Value, tagSettings map[string]string) string // HasIndex check has index or not HasIndex(scope *Scope, tableName string, indexName string) bool diff --git a/dialect_common.go b/dialect_common.go index efc8d642..fc717e17 100644 --- a/dialect_common.go +++ b/dialect_common.go @@ -3,6 +3,7 @@ package gorm import ( "fmt" "reflect" + "strconv" "time" ) @@ -16,17 +17,22 @@ func (commonDialect) Quote(key string) string { return fmt.Sprintf(`"%s"`, key) } -func (commonDialect) DataTypeOf(value reflect.Value, size int, autoIncrease bool) string { - switch value.Kind() { +func (commonDialect) DataTypeOf(dataValue reflect.Value, tagSettings map[string]string) string { + var size int + if num, ok := tagSettings["SIZE"]; ok { + size, _ = strconv.Atoi(num) + } + + switch dataValue.Kind() { case reflect.Bool: return "BOOLEAN" case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: - if autoIncrease { + if _, ok := tagSettings["AUTO_INCREMENT"]; ok { return "INTEGER AUTO_INCREMENT" } return "INTEGER" case reflect.Int64, reflect.Uint64: - if autoIncrease { + if _, ok := tagSettings["AUTO_INCREMENT"]; ok { return "BIGINT AUTO_INCREMENT" } return "BIGINT" @@ -38,18 +44,18 @@ func (commonDialect) DataTypeOf(value reflect.Value, size int, autoIncrease bool } return "VARCHAR(65532)" case reflect.Struct: - if _, ok := value.Interface().(time.Time); ok { + if _, ok := dataValue.Interface().(time.Time); ok { return "TIMESTAMP" } default: - if _, ok := value.Interface().([]byte); ok { + if _, ok := dataValue.Interface().([]byte); ok { if size > 0 && size < 65532 { return fmt.Sprintf("BINARY(%d)", size) } return "BINARY(65532)" } } - panic(fmt.Sprintf("invalid sql type %s (%s) for commonDialect", value.Type().Name(), value.Kind().String())) + panic(fmt.Sprintf("invalid sql type %s (%s) for commonDialect", dataValue.Type().Name(), dataValue.Kind().String())) } func (c commonDialect) HasIndex(scope *Scope, tableName string, indexName string) bool { diff --git a/dialect_mssql.go b/dialect_mssql.go index c3e21c97..d130badb 100644 --- a/dialect_mssql.go +++ b/dialect_mssql.go @@ -3,6 +3,7 @@ package gorm import ( "fmt" "reflect" + "strconv" "time" ) @@ -10,17 +11,22 @@ type mssql struct { commonDialect } -func (mssql) DataTypeOf(value reflect.Value, size int, autoIncrease bool) string { - switch value.Kind() { +func (mssql) DataTypeOf(dataValue reflect.Value, tagSettings map[string]string) string { + var size int + if num, ok := tagSettings["SIZE"]; ok { + size, _ = strconv.Atoi(num) + } + + switch dataValue.Kind() { case reflect.Bool: return "bit" case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: - if autoIncrease { + if _, ok := tagSettings["AUTO_INCREMENT"]; ok { return "int IDENTITY(1,1)" } return "int" case reflect.Int64, reflect.Uint64: - if autoIncrease { + if _, ok := tagSettings["AUTO_INCREMENT"]; ok { return "bigint IDENTITY(1,1)" } return "bigint" @@ -32,18 +38,18 @@ func (mssql) DataTypeOf(value reflect.Value, size int, autoIncrease bool) string } return "text" case reflect.Struct: - if _, ok := value.Interface().(time.Time); ok { + if _, ok := dataValue.Interface().(time.Time); ok { return "datetime2" } default: - if _, ok := value.Interface().([]byte); ok { + if _, ok := dataValue.Interface().([]byte); ok { if size > 0 && size < 65532 { return fmt.Sprintf("varchar(%d)", size) } return "text" } } - panic(fmt.Sprintf("invalid sql type %s (%s) for mssql", value.Type().Name(), value.Kind().String())) + panic(fmt.Sprintf("invalid sql type %s (%s) for mssql", dataValue.Type().Name(), dataValue.Kind().String())) } func (s mssql) HasIndex(scope *Scope, tableName string, indexName string) bool { diff --git a/dialect_mysql.go b/dialect_mysql.go index e334c7a4..acc1f2b7 100644 --- a/dialect_mysql.go +++ b/dialect_mysql.go @@ -3,6 +3,7 @@ package gorm import ( "fmt" "reflect" + "strconv" "time" ) @@ -14,27 +15,32 @@ func (mysql) Quote(key string) string { return fmt.Sprintf("`%s`", key) } -func (mysql) DataTypeOf(value reflect.Value, size int, autoIncrease bool) string { - switch value.Kind() { +func (mysql) DataTypeOf(dataValue reflect.Value, tagSettings map[string]string) string { + var size int + if num, ok := tagSettings["SIZE"]; ok { + size, _ = strconv.Atoi(num) + } + + switch dataValue.Kind() { case reflect.Bool: return "boolean" case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: - if autoIncrease { + if _, ok := tagSettings["AUTO_INCREMENT"]; ok { return "int AUTO_INCREMENT" } return "int" case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: - if autoIncrease { + if _, ok := tagSettings["AUTO_INCREMENT"]; ok { return "int unsigned AUTO_INCREMENT" } return "int unsigned" case reflect.Int64: - if autoIncrease { + if _, ok := tagSettings["AUTO_INCREMENT"]; ok { return "bigint AUTO_INCREMENT" } return "bigint" case reflect.Uint64: - if autoIncrease { + if _, ok := tagSettings["AUTO_INCREMENT"]; ok { return "bigint unsigned AUTO_INCREMENT" } return "bigint unsigned" @@ -46,18 +52,18 @@ func (mysql) DataTypeOf(value reflect.Value, size int, autoIncrease bool) string } return "longtext" case reflect.Struct: - if _, ok := value.Interface().(time.Time); ok { + if _, ok := dataValue.Interface().(time.Time); ok { return "timestamp NULL" } default: - if _, ok := value.Interface().([]byte); ok { + if _, ok := dataValue.Interface().([]byte); ok { if size > 0 && size < 65532 { return fmt.Sprintf("varbinary(%d)", size) } return "longblob" } } - panic(fmt.Sprintf("invalid sql type %s (%s) for mysql", value.Type().Name(), value.Kind().String())) + panic(fmt.Sprintf("invalid sql type %s (%s) for mysql", dataValue.Type().Name(), dataValue.Kind().String())) } func (s mysql) currentDatabase(scope *Scope) (name string) { diff --git a/dialect_postgres.go b/dialect_postgres.go index c4742aec..5215ab96 100644 --- a/dialect_postgres.go +++ b/dialect_postgres.go @@ -5,6 +5,7 @@ import ( "database/sql/driver" "fmt" "reflect" + "strconv" "strings" "time" @@ -19,17 +20,22 @@ func (postgres) BindVar(i int) string { return fmt.Sprintf("$%v", i) } -func (postgres) DataTypeOf(value reflect.Value, size int, autoIncrease bool) string { - switch value.Kind() { +func (postgres) DataTypeOf(dataValue reflect.Value, tagSettings map[string]string) string { + var size int + if num, ok := tagSettings["SIZE"]; ok { + size, _ = strconv.Atoi(num) + } + + switch dataValue.Kind() { case reflect.Bool: return "boolean" case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: - if autoIncrease { + if _, ok := tagSettings["AUTO_INCREMENT"]; ok { return "serial" } return "integer" case reflect.Int64, reflect.Uint64: - if autoIncrease { + if _, ok := tagSettings["AUTO_INCREMENT"]; ok { return "bigserial" } return "bigint" @@ -41,21 +47,21 @@ func (postgres) DataTypeOf(value reflect.Value, size int, autoIncrease bool) str } return "text" case reflect.Struct: - if _, ok := value.Interface().(time.Time); ok { + if _, ok := dataValue.Interface().(time.Time); ok { return "timestamp with time zone" } case reflect.Map: - if value.Type() == hstoreType { + if dataValue.Type() == hstoreType { return "hstore" } default: - if isByteArrayOrSlice(value) { + if isByteArrayOrSlice(dataValue) { return "bytea" - } else if isUUID(value) { + } else if isUUID(dataValue) { return "uuid" } } - panic(fmt.Sprintf("invalid sql type %s (%s) for postgres", value.Type().Name(), value.Kind().String())) + panic(fmt.Sprintf("invalid sql type %s (%s) for postgres", dataValue.Type().Name(), dataValue.Kind().String())) } func (s postgres) HasIndex(scope *Scope, tableName string, indexName string) bool { diff --git a/dialect_sqlite3.go b/dialect_sqlite3.go index e1e35bf7..c838bcc1 100644 --- a/dialect_sqlite3.go +++ b/dialect_sqlite3.go @@ -3,6 +3,7 @@ package gorm import ( "fmt" "reflect" + "strconv" "time" ) @@ -10,17 +11,22 @@ type sqlite3 struct { commonDialect } -func (sqlite3) DataTypeOf(value reflect.Value, size int, autoIncrease bool) string { - switch value.Kind() { +func (sqlite3) DataTypeOf(dataValue reflect.Value, tagSettings map[string]string) string { + var size int + if num, ok := tagSettings["SIZE"]; ok { + size, _ = strconv.Atoi(num) + } + + switch dataValue.Kind() { case reflect.Bool: return "bool" case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: - if autoIncrease { + if _, ok := tagSettings["AUTO_INCREMENT"]; ok { return "integer primary key autoincrement" } return "integer" case reflect.Int64, reflect.Uint64: - if autoIncrease { + if _, ok := tagSettings["AUTO_INCREMENT"]; ok { return "integer primary key autoincrement" } return "bigint" @@ -32,15 +38,15 @@ func (sqlite3) DataTypeOf(value reflect.Value, size int, autoIncrease bool) stri } return "text" case reflect.Struct: - if _, ok := value.Interface().(time.Time); ok { + if _, ok := dataValue.Interface().(time.Time); ok { return "datetime" } default: - if _, ok := value.Interface().([]byte); ok { + if _, ok := dataValue.Interface().([]byte); ok { return "blob" } } - panic(fmt.Sprintf("invalid sql type %s (%s) for sqlite3", value.Type().Name(), value.Kind().String())) + panic(fmt.Sprintf("invalid sql type %s (%s) for sqlite3", dataValue.Type().Name(), dataValue.Kind().String())) } func (s sqlite3) HasIndex(scope *Scope, tableName string, indexName string) bool { diff --git a/model_struct.go b/model_struct.go index c81dcd88..a551d578 100644 --- a/model_struct.go +++ b/model_struct.go @@ -6,7 +6,6 @@ import ( "fmt" "go/ast" "reflect" - "strconv" "strings" "sync" "time" @@ -541,21 +540,7 @@ func (scope *Scope) generateSqlTag(field *StructField) string { } if sqlType == "" { - var size = 255 - - if value, ok := field.TagSettings["SIZE"]; ok { - size, _ = strconv.Atoi(value) - } - - v, autoIncrease := field.TagSettings["AUTO_INCREMENT"] - if field.IsPrimaryKey { - autoIncrease = true - } - if v == "FALSE" { - autoIncrease = false - } - - sqlType = scope.Dialect().DataTypeOf(reflectValue, size, autoIncrease) + sqlType = scope.Dialect().DataTypeOf(reflectValue, field.TagSettings) } if strings.TrimSpace(additionalType) == "" { diff --git a/scope_private.go b/scope_private.go index d5d384af..138bd6fd 100644 --- a/scope_private.go +++ b/scope_private.go @@ -516,11 +516,7 @@ func (scope *Scope) createJoinTable(field *StructField) { for idx, fieldName := range relationship.ForeignFieldNames { if field, ok := scope.Fields()[fieldName]; ok { value := reflect.Indirect(reflect.New(field.Struct.Type)) - primaryKeySqlType := field.TagSettings["TYPE"] - if primaryKeySqlType == "" { - primaryKeySqlType = scope.Dialect().DataTypeOf(value, 255, false) - } - sqlTypes = append(sqlTypes, scope.Quote(relationship.ForeignDBNames[idx])+" "+primaryKeySqlType) + sqlTypes = append(sqlTypes, scope.Quote(relationship.ForeignDBNames[idx])+" "+scope.Dialect().DataTypeOf(value, field.TagSettings)) primaryKeys = append(primaryKeys, scope.Quote(relationship.ForeignDBNames[idx])) } } @@ -528,11 +524,7 @@ func (scope *Scope) createJoinTable(field *StructField) { for idx, fieldName := range relationship.AssociationForeignFieldNames { if field, ok := toScope.Fields()[fieldName]; ok { value := reflect.Indirect(reflect.New(field.Struct.Type)) - primaryKeySqlType := field.TagSettings["TYPE"] - if primaryKeySqlType == "" { - primaryKeySqlType = scope.Dialect().DataTypeOf(value, 255, false) - } - sqlTypes = append(sqlTypes, scope.Quote(relationship.AssociationForeignDBNames[idx])+" "+primaryKeySqlType) + sqlTypes = append(sqlTypes, scope.Quote(relationship.AssociationForeignDBNames[idx])+" "+scope.Dialect().DataTypeOf(value, field.TagSettings)) primaryKeys = append(primaryKeys, scope.Quote(relationship.AssociationForeignDBNames[idx])) } } From 552d9bf4550d378e6b62f85881191173fd0a0f61 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sat, 13 Feb 2016 23:51:36 +0800 Subject: [PATCH 34/83] Refactor DataTypeOf for sqlite --- dialect.go | 42 ++++++++++++++++++++++- dialect_common.go | 9 +++-- dialect_mssql.go | 9 +++-- dialect_mysql.go | 9 +++-- dialect_postgres.go | 9 +++-- dialect_sqlite3.go | 81 ++++++++++++++++++++++++++------------------- model_struct.go | 39 ---------------------- scope_private.go | 22 +++++++----- 8 files changed, 129 insertions(+), 91 deletions(-) diff --git a/dialect.go b/dialect.go index dd3c032e..61220a42 100644 --- a/dialect.go +++ b/dialect.go @@ -1,8 +1,11 @@ package gorm import ( + "database/sql" "fmt" "reflect" + "strconv" + "strings" ) // Dialect interface contains behaviors that differ across SQL database @@ -12,7 +15,7 @@ type Dialect interface { // Quote quotes field name to avoid SQL parsing exceptions by using a reserved word as a field name Quote(key string) string // DataTypeOf return data's sql type - DataTypeOf(value reflect.Value, tagSettings map[string]string) string + DataTypeOf(field *StructField) string // HasIndex check has index or not HasIndex(scope *Scope, tableName string, indexName string) bool @@ -48,3 +51,40 @@ func NewDialect(driver string) Dialect { } return d } + +// ParseFieldStructForDialect parse field struct for dialect +func ParseFieldStructForDialect(field *StructField) (fieldValue reflect.Value, sqlType string, size int, additionalType string) { + // Get redirected field type + var reflectType = field.Struct.Type + for reflectType.Kind() == reflect.Ptr { + reflectType = reflectType.Elem() + } + + // Get redirected field value + fieldValue = reflect.Indirect(reflect.New(reflectType)) + + // Get scanner's real value + var getScannerValue func(reflect.Value) + getScannerValue = func(value reflect.Value) { + fieldValue = value + if _, isScanner := reflect.New(fieldValue.Type()).Interface().(sql.Scanner); isScanner && fieldValue.Kind() == reflect.Struct { + getScannerValue(fieldValue.Field(0)) + } + } + getScannerValue(fieldValue) + + // Default Size + if num, ok := field.TagSettings["SIZE"]; ok { + size, _ = strconv.Atoi(num) + } else { + size = 255 + } + + // Default type from tag setting + additionalType = field.TagSettings["NOT NULL"] + " " + field.TagSettings["UNIQUE"] + if value, ok := field.TagSettings["DEFAULT"]; ok { + additionalType = additionalType + " DEFAULT " + value + } + + return fieldValue, field.TagSettings["TYPE"], size, strings.TrimSpace(additionalType) +} diff --git a/dialect_common.go b/dialect_common.go index fc717e17..f95f3903 100644 --- a/dialect_common.go +++ b/dialect_common.go @@ -17,8 +17,13 @@ func (commonDialect) Quote(key string) string { return fmt.Sprintf(`"%s"`, key) } -func (commonDialect) DataTypeOf(dataValue reflect.Value, tagSettings map[string]string) string { - var size int +func (commonDialect) DataTypeOf(field *StructField) string { + var ( + size int + dataValue = reflect.Indirect(reflect.New(field.Struct.Type)) + tagSettings = field.TagSettings + ) + if num, ok := tagSettings["SIZE"]; ok { size, _ = strconv.Atoi(num) } diff --git a/dialect_mssql.go b/dialect_mssql.go index d130badb..aa3f7f5d 100644 --- a/dialect_mssql.go +++ b/dialect_mssql.go @@ -11,8 +11,13 @@ type mssql struct { commonDialect } -func (mssql) DataTypeOf(dataValue reflect.Value, tagSettings map[string]string) string { - var size int +func (mssql) DataTypeOf(field *StructField) string { + var ( + size int + dataValue = reflect.Indirect(reflect.New(field.Struct.Type)) + tagSettings = field.TagSettings + ) + if num, ok := tagSettings["SIZE"]; ok { size, _ = strconv.Atoi(num) } diff --git a/dialect_mysql.go b/dialect_mysql.go index acc1f2b7..15849abc 100644 --- a/dialect_mysql.go +++ b/dialect_mysql.go @@ -15,8 +15,13 @@ func (mysql) Quote(key string) string { return fmt.Sprintf("`%s`", key) } -func (mysql) DataTypeOf(dataValue reflect.Value, tagSettings map[string]string) string { - var size int +func (mysql) DataTypeOf(field *StructField) string { + var ( + size int + dataValue = reflect.Indirect(reflect.New(field.Struct.Type)) + tagSettings = field.TagSettings + ) + if num, ok := tagSettings["SIZE"]; ok { size, _ = strconv.Atoi(num) } diff --git a/dialect_postgres.go b/dialect_postgres.go index 5215ab96..e49df3d2 100644 --- a/dialect_postgres.go +++ b/dialect_postgres.go @@ -20,8 +20,13 @@ func (postgres) BindVar(i int) string { return fmt.Sprintf("$%v", i) } -func (postgres) DataTypeOf(dataValue reflect.Value, tagSettings map[string]string) string { - var size int +func (postgres) DataTypeOf(field *StructField) string { + var ( + size int + dataValue = reflect.Indirect(reflect.New(field.Struct.Type)) + tagSettings = field.TagSettings + ) + if num, ok := tagSettings["SIZE"]; ok { size, _ = strconv.Atoi(num) } diff --git a/dialect_sqlite3.go b/dialect_sqlite3.go index c838bcc1..0bf2aa8c 100644 --- a/dialect_sqlite3.go +++ b/dialect_sqlite3.go @@ -3,7 +3,7 @@ package gorm import ( "fmt" "reflect" - "strconv" + "strings" "time" ) @@ -11,42 +11,55 @@ type sqlite3 struct { commonDialect } -func (sqlite3) DataTypeOf(dataValue reflect.Value, tagSettings map[string]string) string { - var size int - if num, ok := tagSettings["SIZE"]; ok { - size, _ = strconv.Atoi(num) +// Get Data Type for Sqlite Dialect +func (sqlite3) DataTypeOf(field *StructField) string { + var ( + dataValue, sqlType, size, additionalType = ParseFieldStructForDialect(field) + ) + + if sqlType == "" { + switch dataValue.Kind() { + case reflect.Bool: + sqlType = "bool" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + if field.IsPrimaryKey { + sqlType = "integer primary key autoincrement" + } else { + sqlType = "integer" + } + case reflect.Int64, reflect.Uint64: + if field.IsPrimaryKey { + sqlType = "integer primary key autoincrement" + } else { + sqlType = "bigint" + } + case reflect.Float32, reflect.Float64: + sqlType = "real" + case reflect.String: + if size > 0 && size < 65532 { + sqlType = fmt.Sprintf("varchar(%d)", size) + } else { + sqlType = "text" + } + case reflect.Struct: + if _, ok := dataValue.Interface().(time.Time); ok { + sqlType = "datetime" + } + default: + if _, ok := dataValue.Interface().([]byte); ok { + sqlType = "blob" + } + } } - switch dataValue.Kind() { - case reflect.Bool: - return "bool" - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: - if _, ok := tagSettings["AUTO_INCREMENT"]; ok { - return "integer primary key autoincrement" - } - return "integer" - case reflect.Int64, reflect.Uint64: - if _, ok := tagSettings["AUTO_INCREMENT"]; ok { - return "integer primary key autoincrement" - } - return "bigint" - case reflect.Float32, reflect.Float64: - return "real" - case reflect.String: - if size > 0 && size < 65532 { - return fmt.Sprintf("varchar(%d)", size) - } - return "text" - case reflect.Struct: - if _, ok := dataValue.Interface().(time.Time); ok { - return "datetime" - } - default: - if _, ok := dataValue.Interface().([]byte); ok { - return "blob" - } + if sqlType == "" { + panic(fmt.Sprintf("invalid sql type %s (%s) for sqlite3", dataValue.Type().Name(), dataValue.Kind().String())) } - panic(fmt.Sprintf("invalid sql type %s (%s) for sqlite3", dataValue.Type().Name(), dataValue.Kind().String())) + + if strings.TrimSpace(additionalType) == "" { + return sqlType + } + return fmt.Sprintf("%v %v", sqlType, additionalType) } func (s sqlite3) HasIndex(scope *Scope, tableName string, indexName string) bool { diff --git a/model_struct.go b/model_struct.go index a551d578..a17d2257 100644 --- a/model_struct.go +++ b/model_struct.go @@ -3,7 +3,6 @@ package gorm import ( "database/sql" "errors" - "fmt" "go/ast" "reflect" "strings" @@ -511,44 +510,6 @@ func (scope *Scope) GetStructFields() (fields []*StructField) { return scope.GetModelStruct().StructFields } -func (scope *Scope) generateSqlTag(field *StructField) string { - var sqlType string - structType := field.Struct.Type - if structType.Kind() == reflect.Ptr { - structType = structType.Elem() - } - reflectValue := reflect.Indirect(reflect.New(structType)) - - if value, ok := field.TagSettings["TYPE"]; ok { - sqlType = value - } - - additionalType := field.TagSettings["NOT NULL"] + " " + field.TagSettings["UNIQUE"] - if value, ok := field.TagSettings["DEFAULT"]; ok { - additionalType = additionalType + " DEFAULT " + value - } - - if field.IsScanner { - var getScannerValue func(reflect.Value) - getScannerValue = func(value reflect.Value) { - reflectValue = value - if _, isScanner := reflect.New(reflectValue.Type()).Interface().(sql.Scanner); isScanner && reflectValue.Kind() == reflect.Struct { - getScannerValue(reflectValue.Field(0)) - } - } - getScannerValue(reflectValue) - } - - if sqlType == "" { - sqlType = scope.Dialect().DataTypeOf(reflectValue, field.TagSettings) - } - - if strings.TrimSpace(additionalType) == "" { - return sqlType - } - return fmt.Sprintf("%v %v", sqlType, additionalType) -} - func parseTagSetting(tags reflect.StructTag) map[string]string { setting := map[string]string{} for _, str := range []string{tags.Get("sql"), tags.Get("gorm")} { diff --git a/scope_private.go b/scope_private.go index 4fd48833..d8dd9b93 100644 --- a/scope_private.go +++ b/scope_private.go @@ -511,7 +511,7 @@ func (scope *Scope) getTableOptions() string { return tableOptions.(string) } -func (scope *Scope) createJoinTable(field *StructField) { +func (scope *Scope) createJoinTable(field *Field) { if relationship := field.Relationship; relationship != nil && relationship.JoinTableHandler != nil { joinTableHandler := relationship.JoinTableHandler joinTable := joinTableHandler.Table(scope.db) @@ -521,16 +521,20 @@ func (scope *Scope) createJoinTable(field *StructField) { var sqlTypes, primaryKeys []string for idx, fieldName := range relationship.ForeignFieldNames { if field, ok := scope.Fields()[fieldName]; ok { - value := reflect.Indirect(reflect.New(field.Struct.Type)) - sqlTypes = append(sqlTypes, scope.Quote(relationship.ForeignDBNames[idx])+" "+scope.Dialect().DataTypeOf(value, field.TagSettings)) + foreignKeyStruct := field.StructField.clone() + foreignKeyStruct.IsPrimaryKey = false + foreignKeyStruct.TagSettings["IS_JOINTABLE_FOREIGNKEY"] = "true" + sqlTypes = append(sqlTypes, scope.Quote(relationship.ForeignDBNames[idx])+" "+scope.Dialect().DataTypeOf(foreignKeyStruct)) primaryKeys = append(primaryKeys, scope.Quote(relationship.ForeignDBNames[idx])) } } for idx, fieldName := range relationship.AssociationForeignFieldNames { if field, ok := toScope.Fields()[fieldName]; ok { - value := reflect.Indirect(reflect.New(field.Struct.Type)) - sqlTypes = append(sqlTypes, scope.Quote(relationship.AssociationForeignDBNames[idx])+" "+scope.Dialect().DataTypeOf(value, field.TagSettings)) + foreignKeyStruct := field.StructField.clone() + foreignKeyStruct.IsPrimaryKey = false + foreignKeyStruct.TagSettings["IS_JOINTABLE_FOREIGNKEY"] = "true" + sqlTypes = append(sqlTypes, scope.Quote(relationship.AssociationForeignDBNames[idx])+" "+scope.Dialect().DataTypeOf(foreignKeyStruct)) primaryKeys = append(primaryKeys, scope.Quote(relationship.AssociationForeignDBNames[idx])) } } @@ -545,9 +549,9 @@ func (scope *Scope) createTable() *Scope { var tags []string var primaryKeys []string var primaryKeyInColumnType = false - for _, field := range scope.GetStructFields() { + for _, field := range scope.Fields() { if field.IsNormal { - sqlTag := scope.generateSqlTag(field) + sqlTag := scope.Dialect().DataTypeOf(field.StructField) // Check if the primary key constraint was specified as // part of the column type. If so, we can only support @@ -632,10 +636,10 @@ func (scope *Scope) autoMigrate() *Scope { if !scope.Dialect().HasTable(scope, tableName) { scope.createTable() } else { - for _, field := range scope.GetStructFields() { + for _, field := range scope.Fields() { if !scope.Dialect().HasColumn(scope, tableName, field.DBName) { if field.IsNormal { - sqlTag := scope.generateSqlTag(field) + sqlTag := scope.Dialect().DataTypeOf(field.StructField) scope.Raw(fmt.Sprintf("ALTER TABLE %v ADD %v %v;", quotedTableName, scope.Quote(field.DBName), sqlTag)).Exec() } } From d7455fa5b1aa38c6f2df7483263730dd10473d95 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 14 Feb 2016 13:34:32 +0800 Subject: [PATCH 35/83] Refactor DataTypeOf for mysql --- dialect_mysql.go | 109 ++++++++++++++++++++++++--------------------- dialect_sqlite3.go | 4 +- 2 files changed, 60 insertions(+), 53 deletions(-) diff --git a/dialect_mysql.go b/dialect_mysql.go index 15849abc..22f8b88d 100644 --- a/dialect_mysql.go +++ b/dialect_mysql.go @@ -3,7 +3,7 @@ package gorm import ( "fmt" "reflect" - "strconv" + "strings" "time" ) @@ -15,60 +15,69 @@ func (mysql) Quote(key string) string { return fmt.Sprintf("`%s`", key) } +// Get Data Type for MySQL Dialect func (mysql) DataTypeOf(field *StructField) string { - var ( - size int - dataValue = reflect.Indirect(reflect.New(field.Struct.Type)) - tagSettings = field.TagSettings - ) + var dataValue, sqlType, size, additionalType = ParseFieldStructForDialect(field) - if num, ok := tagSettings["SIZE"]; ok { - size, _ = strconv.Atoi(num) - } - - switch dataValue.Kind() { - case reflect.Bool: - return "boolean" - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: - if _, ok := tagSettings["AUTO_INCREMENT"]; ok { - return "int AUTO_INCREMENT" - } - return "int" - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: - if _, ok := tagSettings["AUTO_INCREMENT"]; ok { - return "int unsigned AUTO_INCREMENT" - } - return "int unsigned" - case reflect.Int64: - if _, ok := tagSettings["AUTO_INCREMENT"]; ok { - return "bigint AUTO_INCREMENT" - } - return "bigint" - case reflect.Uint64: - if _, ok := tagSettings["AUTO_INCREMENT"]; ok { - return "bigint unsigned AUTO_INCREMENT" - } - return "bigint unsigned" - case reflect.Float32, reflect.Float64: - return "double" - case reflect.String: - if size > 0 && size < 65532 { - return fmt.Sprintf("varchar(%d)", size) - } - return "longtext" - case reflect.Struct: - if _, ok := dataValue.Interface().(time.Time); ok { - return "timestamp NULL" - } - default: - if _, ok := dataValue.Interface().([]byte); ok { - if size > 0 && size < 65532 { - return fmt.Sprintf("varbinary(%d)", size) + if sqlType == "" { + switch dataValue.Kind() { + case reflect.Bool: + sqlType = "boolean" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: + if _, ok := field.TagSettings["AUTO_INCREMENT"]; ok || field.IsPrimaryKey { + sqlType = "int AUTO_INCREMENT" + } else { + sqlType = "int" + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + if _, ok := field.TagSettings["AUTO_INCREMENT"]; ok || field.IsPrimaryKey { + sqlType = "int unsigned AUTO_INCREMENT" + } else { + sqlType = "int unsigned" + } + case reflect.Int64: + if _, ok := field.TagSettings["AUTO_INCREMENT"]; ok || field.IsPrimaryKey { + sqlType = "bigint AUTO_INCREMENT" + } else { + sqlType = "bigint" + } + case reflect.Uint64: + if _, ok := field.TagSettings["AUTO_INCREMENT"]; ok || field.IsPrimaryKey { + sqlType = "bigint unsigned AUTO_INCREMENT" + } else { + sqlType = "bigint unsigned" + } + case reflect.Float32, reflect.Float64: + sqlType = "double" + case reflect.String: + if size > 0 && size < 65532 { + sqlType = fmt.Sprintf("varchar(%d)", size) + } else { + sqlType = "longtext" + } + case reflect.Struct: + if _, ok := dataValue.Interface().(time.Time); ok { + sqlType = "timestamp NULL" + } + default: + if _, ok := dataValue.Interface().([]byte); ok { + if size > 0 && size < 65532 { + sqlType = fmt.Sprintf("varbinary(%d)", size) + } else { + sqlType = "longblob" + } } - return "longblob" } } - panic(fmt.Sprintf("invalid sql type %s (%s) for mysql", dataValue.Type().Name(), dataValue.Kind().String())) + + if sqlType == "" { + panic(fmt.Sprintf("invalid sql type %s (%s) for mysql", dataValue.Type().Name(), dataValue.Kind().String())) + } + + if strings.TrimSpace(additionalType) == "" { + return sqlType + } + return fmt.Sprintf("%v %v", sqlType, additionalType) } func (s mysql) currentDatabase(scope *Scope) (name string) { diff --git a/dialect_sqlite3.go b/dialect_sqlite3.go index 0bf2aa8c..d5ffb78d 100644 --- a/dialect_sqlite3.go +++ b/dialect_sqlite3.go @@ -13,9 +13,7 @@ type sqlite3 struct { // Get Data Type for Sqlite Dialect func (sqlite3) DataTypeOf(field *StructField) string { - var ( - dataValue, sqlType, size, additionalType = ParseFieldStructForDialect(field) - ) + var dataValue, sqlType, size, additionalType = ParseFieldStructForDialect(field) if sqlType == "" { switch dataValue.Kind() { From b4abd125c16ab55f3dfbdd5be5ecbc755ae7df94 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 14 Feb 2016 13:51:34 +0800 Subject: [PATCH 36/83] Refactor DataTypeOf for postgres, mssql --- dialect_common.go | 86 +++++++++++++++++++++++-------------------- dialect_mssql.go | 86 +++++++++++++++++++++++-------------------- dialect_postgres.go | 90 +++++++++++++++++++++++---------------------- 3 files changed, 139 insertions(+), 123 deletions(-) diff --git a/dialect_common.go b/dialect_common.go index f95f3903..9f10a287 100644 --- a/dialect_common.go +++ b/dialect_common.go @@ -3,7 +3,7 @@ package gorm import ( "fmt" "reflect" - "strconv" + "strings" "time" ) @@ -18,49 +18,55 @@ func (commonDialect) Quote(key string) string { } func (commonDialect) DataTypeOf(field *StructField) string { - var ( - size int - dataValue = reflect.Indirect(reflect.New(field.Struct.Type)) - tagSettings = field.TagSettings - ) + var dataValue, sqlType, size, additionalType = ParseFieldStructForDialect(field) - if num, ok := tagSettings["SIZE"]; ok { - size, _ = strconv.Atoi(num) - } - - switch dataValue.Kind() { - case reflect.Bool: - return "BOOLEAN" - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: - if _, ok := tagSettings["AUTO_INCREMENT"]; ok { - return "INTEGER AUTO_INCREMENT" - } - return "INTEGER" - case reflect.Int64, reflect.Uint64: - if _, ok := tagSettings["AUTO_INCREMENT"]; ok { - return "BIGINT AUTO_INCREMENT" - } - return "BIGINT" - case reflect.Float32, reflect.Float64: - return "FLOAT" - case reflect.String: - if size > 0 && size < 65532 { - return fmt.Sprintf("VARCHAR(%d)", size) - } - return "VARCHAR(65532)" - case reflect.Struct: - if _, ok := dataValue.Interface().(time.Time); ok { - return "TIMESTAMP" - } - default: - if _, ok := dataValue.Interface().([]byte); ok { - if size > 0 && size < 65532 { - return fmt.Sprintf("BINARY(%d)", size) + if sqlType == "" { + switch dataValue.Kind() { + case reflect.Bool: + sqlType = "BOOLEAN" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + if _, ok := field.TagSettings["AUTO_INCREMENT"]; ok { + sqlType = "INTEGER AUTO_INCREMENT" + } else { + sqlType = "INTEGER" + } + case reflect.Int64, reflect.Uint64: + if _, ok := field.TagSettings["AUTO_INCREMENT"]; ok { + sqlType = "BIGINT AUTO_INCREMENT" + } else { + sqlType = "BIGINT" + } + case reflect.Float32, reflect.Float64: + sqlType = "FLOAT" + case reflect.String: + if size > 0 && size < 65532 { + sqlType = fmt.Sprintf("VARCHAR(%d)", size) + } else { + sqlType = "VARCHAR(65532)" + } + case reflect.Struct: + if _, ok := dataValue.Interface().(time.Time); ok { + sqlType = "TIMESTAMP" + } + default: + if _, ok := dataValue.Interface().([]byte); ok { + if size > 0 && size < 65532 { + sqlType = fmt.Sprintf("BINARY(%d)", size) + } else { + sqlType = "BINARY(65532)" + } } - return "BINARY(65532)" } } - panic(fmt.Sprintf("invalid sql type %s (%s) for commonDialect", dataValue.Type().Name(), dataValue.Kind().String())) + + if sqlType == "" { + panic(fmt.Sprintf("invalid sql type %s (%s) for commonDialect", dataValue.Type().Name(), dataValue.Kind().String())) + } + + if strings.TrimSpace(additionalType) == "" { + return sqlType + } + return fmt.Sprintf("%v %v", sqlType, additionalType) } func (c commonDialect) HasIndex(scope *Scope, tableName string, indexName string) bool { diff --git a/dialect_mssql.go b/dialect_mssql.go index aa3f7f5d..63971e34 100644 --- a/dialect_mssql.go +++ b/dialect_mssql.go @@ -3,7 +3,7 @@ package gorm import ( "fmt" "reflect" - "strconv" + "strings" "time" ) @@ -12,49 +12,55 @@ type mssql struct { } func (mssql) DataTypeOf(field *StructField) string { - var ( - size int - dataValue = reflect.Indirect(reflect.New(field.Struct.Type)) - tagSettings = field.TagSettings - ) + var dataValue, sqlType, size, additionalType = ParseFieldStructForDialect(field) - if num, ok := tagSettings["SIZE"]; ok { - size, _ = strconv.Atoi(num) - } - - switch dataValue.Kind() { - case reflect.Bool: - return "bit" - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: - if _, ok := tagSettings["AUTO_INCREMENT"]; ok { - return "int IDENTITY(1,1)" - } - return "int" - case reflect.Int64, reflect.Uint64: - if _, ok := tagSettings["AUTO_INCREMENT"]; ok { - return "bigint IDENTITY(1,1)" - } - return "bigint" - case reflect.Float32, reflect.Float64: - return "float" - case reflect.String: - if size > 0 && size < 65532 { - return fmt.Sprintf("nvarchar(%d)", size) - } - return "text" - case reflect.Struct: - if _, ok := dataValue.Interface().(time.Time); ok { - return "datetime2" - } - default: - if _, ok := dataValue.Interface().([]byte); ok { - if size > 0 && size < 65532 { - return fmt.Sprintf("varchar(%d)", size) + if sqlType == "" { + switch dataValue.Kind() { + case reflect.Bool: + sqlType = "bit" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + if _, ok := field.TagSettings["AUTO_INCREMENT"]; ok || field.IsPrimaryKey { + sqlType = "int IDENTITY(1,1)" + } else { + sqlType = "int" + } + case reflect.Int64, reflect.Uint64: + if _, ok := field.TagSettings["AUTO_INCREMENT"]; ok || field.IsPrimaryKey { + sqlType = "bigint IDENTITY(1,1)" + } else { + sqlType = "bigint" + } + case reflect.Float32, reflect.Float64: + sqlType = "float" + case reflect.String: + if size > 0 && size < 65532 { + sqlType = fmt.Sprintf("nvarchar(%d)", size) + } else { + sqlType = "text" + } + case reflect.Struct: + if _, ok := dataValue.Interface().(time.Time); ok { + sqlType = "datetime2" + } + default: + if _, ok := dataValue.Interface().([]byte); ok { + if size > 0 && size < 65532 { + sqlType = fmt.Sprintf("varchar(%d)", size) + } else { + sqlType = "text" + } } - return "text" } } - panic(fmt.Sprintf("invalid sql type %s (%s) for mssql", dataValue.Type().Name(), dataValue.Kind().String())) + + if sqlType == "" { + panic(fmt.Sprintf("invalid sql type %s (%s) for mssql", dataValue.Type().Name(), dataValue.Kind().String())) + } + + if strings.TrimSpace(additionalType) == "" { + return sqlType + } + return fmt.Sprintf("%v %v", sqlType, additionalType) } func (s mssql) HasIndex(scope *Scope, tableName string, indexName string) bool { diff --git a/dialect_postgres.go b/dialect_postgres.go index e49df3d2..b35b918a 100644 --- a/dialect_postgres.go +++ b/dialect_postgres.go @@ -5,7 +5,6 @@ import ( "database/sql/driver" "fmt" "reflect" - "strconv" "strings" "time" @@ -21,52 +20,57 @@ func (postgres) BindVar(i int) string { } func (postgres) DataTypeOf(field *StructField) string { - var ( - size int - dataValue = reflect.Indirect(reflect.New(field.Struct.Type)) - tagSettings = field.TagSettings - ) + var dataValue, sqlType, size, additionalType = ParseFieldStructForDialect(field) - if num, ok := tagSettings["SIZE"]; ok { - size, _ = strconv.Atoi(num) + if sqlType == "" { + switch dataValue.Kind() { + case reflect.Bool: + sqlType = "boolean" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + if _, ok := field.TagSettings["AUTO_INCREMENT"]; ok || field.IsPrimaryKey { + sqlType = "serial" + } else { + sqlType = "integer" + } + case reflect.Int64, reflect.Uint64: + if _, ok := field.TagSettings["AUTO_INCREMENT"]; ok || field.IsPrimaryKey { + sqlType = "bigserial" + } else { + sqlType = "bigint" + } + case reflect.Float32, reflect.Float64: + sqlType = "numeric" + case reflect.String: + if size > 0 && size < 65532 { + sqlType = fmt.Sprintf("varchar(%d)", size) + } else { + sqlType = "text" + } + case reflect.Struct: + if _, ok := dataValue.Interface().(time.Time); ok { + sqlType = "timestamp with time zone" + } + case reflect.Map: + if dataValue.Type() == hstoreType { + sqlType = "hstore" + } + default: + if isByteArrayOrSlice(dataValue) { + sqlType = "bytea" + } else if isUUID(dataValue) { + sqlType = "uuid" + } + } } - switch dataValue.Kind() { - case reflect.Bool: - return "boolean" - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: - if _, ok := tagSettings["AUTO_INCREMENT"]; ok { - return "serial" - } - return "integer" - case reflect.Int64, reflect.Uint64: - if _, ok := tagSettings["AUTO_INCREMENT"]; ok { - return "bigserial" - } - return "bigint" - case reflect.Float32, reflect.Float64: - return "numeric" - case reflect.String: - if size > 0 && size < 65532 { - return fmt.Sprintf("varchar(%d)", size) - } - return "text" - case reflect.Struct: - if _, ok := dataValue.Interface().(time.Time); ok { - return "timestamp with time zone" - } - case reflect.Map: - if dataValue.Type() == hstoreType { - return "hstore" - } - default: - if isByteArrayOrSlice(dataValue) { - return "bytea" - } else if isUUID(dataValue) { - return "uuid" - } + if sqlType == "" { + panic(fmt.Sprintf("invalid sql type %s (%s) for postgres", dataValue.Type().Name(), dataValue.Kind().String())) } - panic(fmt.Sprintf("invalid sql type %s (%s) for postgres", dataValue.Type().Name(), dataValue.Kind().String())) + + if strings.TrimSpace(additionalType) == "" { + return sqlType + } + return fmt.Sprintf("%v %v", sqlType, additionalType) } func (s postgres) HasIndex(scope *Scope, tableName string, indexName string) bool { From ea40d075fecc2536071d36e869fe6e62bedcd455 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 14 Feb 2016 16:17:02 +0800 Subject: [PATCH 37/83] Fix migration --- scope_private.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scope_private.go b/scope_private.go index d8dd9b93..8e148820 100644 --- a/scope_private.go +++ b/scope_private.go @@ -511,7 +511,7 @@ func (scope *Scope) getTableOptions() string { return tableOptions.(string) } -func (scope *Scope) createJoinTable(field *Field) { +func (scope *Scope) createJoinTable(field *StructField) { if relationship := field.Relationship; relationship != nil && relationship.JoinTableHandler != nil { joinTableHandler := relationship.JoinTableHandler joinTable := joinTableHandler.Table(scope.db) @@ -521,7 +521,7 @@ func (scope *Scope) createJoinTable(field *Field) { var sqlTypes, primaryKeys []string for idx, fieldName := range relationship.ForeignFieldNames { if field, ok := scope.Fields()[fieldName]; ok { - foreignKeyStruct := field.StructField.clone() + foreignKeyStruct := field.clone() foreignKeyStruct.IsPrimaryKey = false foreignKeyStruct.TagSettings["IS_JOINTABLE_FOREIGNKEY"] = "true" sqlTypes = append(sqlTypes, scope.Quote(relationship.ForeignDBNames[idx])+" "+scope.Dialect().DataTypeOf(foreignKeyStruct)) @@ -531,7 +531,7 @@ func (scope *Scope) createJoinTable(field *Field) { for idx, fieldName := range relationship.AssociationForeignFieldNames { if field, ok := toScope.Fields()[fieldName]; ok { - foreignKeyStruct := field.StructField.clone() + foreignKeyStruct := field.clone() foreignKeyStruct.IsPrimaryKey = false foreignKeyStruct.TagSettings["IS_JOINTABLE_FOREIGNKEY"] = "true" sqlTypes = append(sqlTypes, scope.Quote(relationship.AssociationForeignDBNames[idx])+" "+scope.Dialect().DataTypeOf(foreignKeyStruct)) @@ -549,9 +549,9 @@ func (scope *Scope) createTable() *Scope { var tags []string var primaryKeys []string var primaryKeyInColumnType = false - for _, field := range scope.Fields() { + for _, field := range scope.GetModelStruct().StructFields { if field.IsNormal { - sqlTag := scope.Dialect().DataTypeOf(field.StructField) + sqlTag := scope.Dialect().DataTypeOf(field) // Check if the primary key constraint was specified as // part of the column type. If so, we can only support @@ -636,10 +636,10 @@ func (scope *Scope) autoMigrate() *Scope { if !scope.Dialect().HasTable(scope, tableName) { scope.createTable() } else { - for _, field := range scope.Fields() { + for _, field := range scope.GetModelStruct().StructFields { if !scope.Dialect().HasColumn(scope, tableName, field.DBName) { if field.IsNormal { - sqlTag := scope.Dialect().DataTypeOf(field.StructField) + sqlTag := scope.Dialect().DataTypeOf(field) scope.Raw(fmt.Sprintf("ALTER TABLE %v ADD %v %v;", quotedTableName, scope.Quote(field.DBName), sqlTag)).Exec() } } From 421979cfc245aafa5af900ac815d0913aac40994 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 14 Feb 2016 17:21:40 +0800 Subject: [PATCH 38/83] Order results when preload many2many relations --- callback_query_preload.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/callback_query_preload.go b/callback_query_preload.go index 75225c27..97591915 100644 --- a/callback_query_preload.go +++ b/callback_query_preload.go @@ -188,7 +188,7 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface var ( relation = field.Relationship joinTableHandler = relation.JoinTableHandler - fieldType = field.StructField.Struct.Type.Elem() + fieldType = field.Struct.Type.Elem() foreignKeyValue interface{} foreignKeyType = reflect.ValueOf(&foreignKeyValue).Type() linkHash = map[string][]reflect.Value{} @@ -206,9 +206,14 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface } // generate query with join table - preloadJoinDB := scope.NewDB().Table(scope.New(reflect.New(fieldType).Interface()).TableName()).Select("*") + newScope := scope.New(reflect.New(fieldType).Interface()) + preloadJoinDB := scope.NewDB().Table(newScope.TableName()).Select("*") preloadJoinDB = joinTableHandler.JoinWith(joinTableHandler, preloadJoinDB, scope.Value) + if primaryField := newScope.PrimaryField(); primaryField != nil { + preloadJoinDB = preloadJoinDB.Order(fmt.Sprintf("%v.%v %v", newScope.QuotedTableName(), newScope.Quote(primaryField.DBName), "ASC")) + } + // preload inline conditions if len(conditions) > 0 { preloadJoinDB = preloadJoinDB.Where(conditions[0], conditions[1:]...) From f4456e139e5cdd7288ae60fac8b1dd903630b88b Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 14 Feb 2016 18:06:42 +0800 Subject: [PATCH 39/83] Register dialects --- dialect.go | 27 ++++++++++++--------------- dialect_common.go | 4 ++++ dialect_mssql.go | 4 ++++ dialect_mysql.go | 4 ++++ dialect_postgres.go | 4 ++++ dialect_sqlite3.go | 5 +++++ main.go | 5 +---- 7 files changed, 34 insertions(+), 19 deletions(-) diff --git a/dialect.go b/dialect.go index 61220a42..1923e66e 100644 --- a/dialect.go +++ b/dialect.go @@ -34,22 +34,19 @@ type Dialect interface { LastInsertIdReturningSuffix(tableName, columnName string) string } -func NewDialect(driver string) Dialect { - var d Dialect - switch driver { - case "postgres": - d = &postgres{} - case "mysql": - d = &mysql{} - case "sqlite3": - d = &sqlite3{} - case "mssql": - d = &mssql{} - default: - fmt.Printf("`%v` is not officially supported, running under compatibility mode.\n", driver) - d = &commonDialect{} +var dialectsMap = map[string]Dialect{} + +func newDialect(name string) Dialect { + if dialect, ok := dialectsMap[name]; ok { + return dialect } - return d + fmt.Printf("`%v` is not officially supported, running under compatibility mode.\n", name) + return &commonDialect{} +} + +// RegisterDialect register new dialect +func RegisterDialect(name string, dialect Dialect) { + dialectsMap[name] = dialect } // ParseFieldStructForDialect parse field struct for dialect diff --git a/dialect_common.go b/dialect_common.go index 9f10a287..d5a81ad6 100644 --- a/dialect_common.go +++ b/dialect_common.go @@ -9,6 +9,10 @@ import ( type commonDialect struct{} +func init() { + RegisterDialect("common", &commonDialect{}) +} + func (commonDialect) BindVar(i int) string { return "$$" // ? } diff --git a/dialect_mssql.go b/dialect_mssql.go index 63971e34..a2af49ad 100644 --- a/dialect_mssql.go +++ b/dialect_mssql.go @@ -11,6 +11,10 @@ type mssql struct { commonDialect } +func init() { + RegisterDialect("mssql", &mssql{}) +} + func (mssql) DataTypeOf(field *StructField) string { var dataValue, sqlType, size, additionalType = ParseFieldStructForDialect(field) diff --git a/dialect_mysql.go b/dialect_mysql.go index 22f8b88d..51b1926a 100644 --- a/dialect_mysql.go +++ b/dialect_mysql.go @@ -11,6 +11,10 @@ type mysql struct { commonDialect } +func init() { + RegisterDialect("mysql", &mysql{}) +} + func (mysql) Quote(key string) string { return fmt.Sprintf("`%s`", key) } diff --git a/dialect_postgres.go b/dialect_postgres.go index b35b918a..e726d233 100644 --- a/dialect_postgres.go +++ b/dialect_postgres.go @@ -15,6 +15,10 @@ type postgres struct { commonDialect } +func init() { + RegisterDialect("postgres", &postgres{}) +} + func (postgres) BindVar(i int) string { return fmt.Sprintf("$%v", i) } diff --git a/dialect_sqlite3.go b/dialect_sqlite3.go index d5ffb78d..3abdb92e 100644 --- a/dialect_sqlite3.go +++ b/dialect_sqlite3.go @@ -11,6 +11,11 @@ type sqlite3 struct { commonDialect } +func init() { + RegisterDialect("sqlite", &sqlite3{}) + RegisterDialect("sqlite3", &sqlite3{}) +} + // Get Data Type for Sqlite Dialect func (sqlite3) DataTypeOf(field *StructField) string { var dataValue, sqlType, size, additionalType = ParseFieldStructForDialect(field) diff --git a/main.go b/main.go index 4cdbca0b..5f4d9dbd 100644 --- a/main.go +++ b/main.go @@ -55,9 +55,6 @@ func Open(dialect string, args ...interface{}) (*DB, error) { driver = value source = args[1].(string) } - if driver == "foundation" { - driver = "postgres" // FoundationDB speaks a postgres-compatible protocol. - } dbSql, err = sql.Open(driver, source) case sqlCommon: source = reflect.Indirect(reflect.ValueOf(value)).FieldByName("dsn").String() @@ -65,7 +62,7 @@ func Open(dialect string, args ...interface{}) (*DB, error) { } db = DB{ - dialect: NewDialect(dialect), + dialect: newDialect(dialect), logger: defaultLogger, callbacks: defaultCallback, source: source, From a7097106b18f4e88ae2c3d5d0c5b927b52cf21a5 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 14 Feb 2016 22:42:17 +0800 Subject: [PATCH 40/83] Fix #784 set mysql datetime's type to NOT NULL --- dialect_mysql.go | 6 +++++- structs_test.go | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/dialect_mysql.go b/dialect_mysql.go index 51b1926a..10d63db2 100644 --- a/dialect_mysql.go +++ b/dialect_mysql.go @@ -61,7 +61,11 @@ func (mysql) DataTypeOf(field *StructField) string { } case reflect.Struct: if _, ok := dataValue.Interface().(time.Time); ok { - sqlType = "timestamp NULL" + if _, ok := field.TagSettings["NOT NULL"]; ok { + sqlType = "timestamp" + } else { + sqlType = "timestamp NULL" + } } default: if _, ok := dataValue.Interface().([]byte); ok { diff --git a/structs_test.go b/structs_test.go index 42eb6bc3..e7bae25f 100644 --- a/structs_test.go +++ b/structs_test.go @@ -42,7 +42,7 @@ type CreditCard struct { ID int8 Number string UserId sql.NullInt64 - CreatedAt time.Time + CreatedAt time.Time `sql:"not null"` UpdatedAt time.Time DeletedAt *time.Time } From 6546ec3b5e52d1bd9952da38095185132f218bb7 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 14 Feb 2016 23:29:06 +0800 Subject: [PATCH 41/83] Scan Rows into struct, fix #59 --- README.md | 157 +++++++++++++++++++++++++---------------------- main.go | 14 +++++ main_test.go | 36 ++++++++++- utils.go | 128 ++++++++++++++++++++++++++++++++++++++ utils_private.go | 131 --------------------------------------- 5 files changed, 261 insertions(+), 205 deletions(-) delete mode 100644 utils_private.go diff --git a/README.md b/README.md index b0dcb7bd..9535015e 100644 --- a/README.md +++ b/README.md @@ -68,15 +68,15 @@ go get -u github.com/jinzhu/gorm - [Limit](#limit) - [Offset](#offset) - [Count](#count) - - [Pluck](#pluck) - - [Raw SQL](#raw-sql) - - [Row & Rows](#row--rows) - - [Scan](#scan) - [Group & Having](#group--having) - [Joins](#joins) - [Transactions](#transactions) - [Scopes](#scopes) - [Callbacks](#callbacks) + - [Pluck](#pluck) + - [Scan](#scan) + - [Raw SQL](#raw-sql) + - [Row & Rows](#row--rows) - [Specifying The Table Name](#specifying-the-table-name) - [Error Handling](#error-handling) - [Logger](#logger) @@ -866,73 +866,6 @@ db.Table("deleted_users").Count(&count) //// SELECT count(*) FROM deleted_users; ``` -## Pluck - -Get selected attributes as map - -```go -var ages []int64 -db.Find(&users).Pluck("age", &ages) - -var names []string -db.Model(&User{}).Pluck("name", &names) - -db.Table("deleted_users").Pluck("name", &names) - -// Requesting more than one column? Do it like this: -db.Select("name, age").Find(&users) -``` - -## Raw SQL - -```go -db.Exec("DROP TABLE users;") -db.Exec("UPDATE orders SET shipped_at=? WHERE id IN (?)", time.Now, []int64{11,22,33}) -``` - -## Row & Rows - -It is even possible to get query result as `*sql.Row` or `*sql.Rows` - -```go -row := db.Table("users").Where("name = ?", "jinzhu").Select("name, age").Row() // (*sql.Row) -row.Scan(&name, &age) - -rows, err := db.Model(User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error) -defer rows.Close() -for rows.Next() { - ... - rows.Scan(&name, &age, &email) - ... -} - -// Raw SQL -rows, err := db.Raw("select name, age, email from users where name = ?", "jinzhu").Rows() // (*sql.Rows, error) -defer rows.Close() -for rows.Next() { - ... - rows.Scan(&name, &age, &email) - ... -} -``` - -## Scan - -Scan results into another struct. - -```go -type Result struct { - Name string - Age int -} - -var result Result -db.Table("users").Select("name, age").Where("name = ?", 3).Scan(&result) - -// Raw SQL -db.Raw("SELECT name, age FROM users WHERE name = ?", 3).Scan(&result) -``` - ## Group & Having ```go @@ -1108,7 +1041,7 @@ func (u *User) AfterCreate() (err error) { } ``` -Save/delete operations in gorm are running in a transaction. +Save/delete operations in gorm are running in a transaction. Changes made in that transaction are not visible unless it is commited. So if you want to use those changes in your callbacks, you need to run your SQL in the same transaction. For this Gorm supports passing transactions to callbacks like this: @@ -1120,6 +1053,86 @@ func (u *User) AfterCreate(tx *gorm.DB) (err error) { } ``` +## Pluck + +Get selected attributes as map + +```go +var ages []int64 +db.Find(&users).Pluck("age", &ages) + +var names []string +db.Model(&User{}).Pluck("name", &names) + +db.Table("deleted_users").Pluck("name", &names) + +// Requesting more than one column? Do it like this: +db.Select("name, age").Find(&users) +``` + +## Scan + +Scan results into another struct. + +```go +type Result struct { + Name string + Age int +} + +var result Result +db.Table("users").Select("name, age").Where("name = ?", 3).Scan(&result) + +// Raw SQL +db.Raw("SELECT name, age FROM users WHERE name = ?", 3).Scan(&result) +``` + +## Raw SQL + +```go +db.Exec("DROP TABLE users;") +db.Exec("UPDATE orders SET shipped_at=? WHERE id IN (?)", time.Now, []int64{11,22,33}) +``` + +## Row & Rows + +It is even possible to get query result as `*sql.Row` or `*sql.Rows` + +```go +row := db.Table("users").Where("name = ?", "jinzhu").Select("name, age").Row() // (*sql.Row) +row.Scan(&name, &age) + +rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error) +defer rows.Close() +for rows.Next() { + ... + rows.Scan(&name, &age, &email) + ... +} + +// Raw SQL +rows, err := db.Raw("select name, age, email from users where name = ?", "jinzhu").Rows() // (*sql.Rows, error) +defer rows.Close() +for rows.Next() { + ... + rows.Scan(&name, &age, &email) + ... +} +``` + +### Scan Rows + +```go +rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error) +defer rows.Close() + +for rows.Next() { + var user User + db.ScanRows(rows, &user) + // do something +} +``` + ## Specifying The Table Name ```go diff --git a/main.go b/main.go index 5f4d9dbd..51bd9914 100644 --- a/main.go +++ b/main.go @@ -224,6 +224,20 @@ func (s *DB) Rows() (*sql.Rows, error) { return s.NewScope(s.Value).rows() } +func (s *DB) ScanRows(rows *sql.Rows, value interface{}) error { + var ( + clone = s.clone() + scope = clone.NewScope(value) + columns, err = rows.Columns() + ) + + if clone.AddError(err) == nil { + scope.scan(rows, columns, scope.Fields()) + } + + return clone.Error +} + func (s *DB) Pluck(column string, value interface{}) *DB { return s.NewScope(s.Value).pluck(column, value).db } diff --git a/main_test.go b/main_test.go index c07d474b..97a3d84e 100644 --- a/main_test.go +++ b/main_test.go @@ -4,6 +4,7 @@ import ( "database/sql" "database/sql/driver" "fmt" + "reflect" "strconv" _ "github.com/denisenkom/go-mssqldb" @@ -376,7 +377,7 @@ func TestRows(t *testing.T) { rows, err := DB.Table("users").Where("name = ? or name = ?", user2.Name, user3.Name).Select("name, age").Rows() if err != nil { - t.Errorf("Not error should happen, but got") + t.Errorf("Not error should happen, got %v", err) } count := 0 @@ -386,8 +387,39 @@ func TestRows(t *testing.T) { rows.Scan(&name, &age) count++ } + if count != 2 { - t.Errorf("Should found two records with name 3") + t.Errorf("Should found two records") + } +} + +func TestScanRows(t *testing.T) { + user1 := User{Name: "ScanRowsUser1", Age: 1, Birthday: now.MustParse("2000-1-1")} + user2 := User{Name: "ScanRowsUser2", Age: 10, Birthday: now.MustParse("2010-1-1")} + user3 := User{Name: "ScanRowsUser3", Age: 20, Birthday: now.MustParse("2020-1-1")} + DB.Save(&user1).Save(&user2).Save(&user3) + + rows, err := DB.Table("users").Where("name = ? or name = ?", user2.Name, user3.Name).Select("name, age").Rows() + if err != nil { + t.Errorf("Not error should happen, got %v", err) + } + + type Result struct { + Name string + Age int + } + + var results []Result + for rows.Next() { + var result Result + if err := DB.ScanRows(rows, &result); err != nil { + t.Errorf("should get no error, but got %v", err) + } + results = append(results, result) + } + + if !reflect.DeepEqual(results, []Result{{Name: "ScanRowsUser2", Age: 10}, {Name: "ScanRowsUser3", Age: 20}}) { + t.Errorf("Should find expected results") } } diff --git a/utils.go b/utils.go index 9d2bb075..94e345cc 100644 --- a/utils.go +++ b/utils.go @@ -2,8 +2,11 @@ package gorm import ( "bytes" + "database/sql/driver" "fmt" "reflect" + "regexp" + "runtime" "strings" "sync" ) @@ -50,6 +53,7 @@ const ( upper strCase = true ) +// ToDBName convert string to db name func ToDBName(name string) string { if v := smap.Get(name); v != "" { return v @@ -94,11 +98,14 @@ func ToDBName(name string) string { return s } +// SQL expression type expr struct { expr string args []interface{} } +// Expr generate raw SQL expression for SQL, for example: +// DB.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100)) func Expr(expression string, args ...interface{}) *expr { return &expr{expr: expression, args: args} } @@ -148,3 +155,124 @@ func toQueryValues(values [][]interface{}) (results []interface{}) { } return } + +func fileWithLineNum() string { + for i := 2; i < 15; i++ { + _, file, line, ok := runtime.Caller(i) + if ok && (!regexp.MustCompile(`jinzhu/gorm/.*.go`).MatchString(file) || regexp.MustCompile(`jinzhu/gorm/.*test.go`).MatchString(file)) { + return fmt.Sprintf("%v:%v", file, line) + } + } + return "" +} + +func isBlank(value reflect.Value) bool { + return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface()) +} + +func toSearchableMap(attrs ...interface{}) (result interface{}) { + if len(attrs) > 1 { + if str, ok := attrs[0].(string); ok { + result = map[string]interface{}{str: attrs[1]} + } + } else if len(attrs) == 1 { + if attr, ok := attrs[0].(map[string]interface{}); ok { + result = attr + } + + if attr, ok := attrs[0].(interface{}); ok { + result = attr + } + } + return +} + +func convertInterfaceToMap(values interface{}) map[string]interface{} { + attrs := map[string]interface{}{} + + switch value := values.(type) { + case map[string]interface{}: + for k, v := range value { + attrs[k] = v + } + case []interface{}: + for _, v := range value { + for key, value := range convertInterfaceToMap(v) { + attrs[key] = value + } + } + case interface{}: + reflectValue := reflect.ValueOf(values) + + switch reflectValue.Kind() { + case reflect.Map: + for _, key := range reflectValue.MapKeys() { + attrs[ToDBName(key.Interface().(string))] = reflectValue.MapIndex(key).Interface() + } + default: + for _, field := range (&Scope{Value: values}).Fields() { + if !field.IsBlank && !field.IsIgnored { + attrs[field.DBName] = field.Field.Interface() + } + } + } + } + return attrs +} + +func equalAsString(a interface{}, b interface{}) bool { + return toString(a) == toString(b) +} + +func toString(str interface{}) string { + if values, ok := str.([]interface{}); ok { + var results []string + for _, value := range values { + results = append(results, toString(value)) + } + return strings.Join(results, "_") + } else if bytes, ok := str.([]byte); ok { + return string(bytes) + } else if reflectValue := reflect.Indirect(reflect.ValueOf(str)); reflectValue.IsValid() { + return fmt.Sprintf("%v", reflectValue.Interface()) + } + return "" +} + +func makeSlice(elemType reflect.Type) interface{} { + if elemType.Kind() == reflect.Slice { + elemType = elemType.Elem() + } + sliceType := reflect.SliceOf(elemType) + slice := reflect.New(sliceType) + slice.Elem().Set(reflect.MakeSlice(sliceType, 0, 0)) + return slice.Interface() +} + +func strInSlice(a string, list []string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} + +// getValueFromFields return given fields's value +func getValueFromFields(value reflect.Value, fieldNames []string) (results []interface{}) { + // If value is a nil pointer, Indirect returns a zero Value! + // Therefor we need to check for a zero value, + // as FieldByName could panic + if indirectValue := reflect.Indirect(value); indirectValue.IsValid() { + for _, fieldName := range fieldNames { + if fieldValue := indirectValue.FieldByName(fieldName); fieldValue.IsValid() { + result := fieldValue.Interface() + if r, ok := result.(driver.Valuer); ok { + result, _ = r.Value() + } + results = append(results, result) + } + } + } + return +} diff --git a/utils_private.go b/utils_private.go deleted file mode 100644 index 2851a37e..00000000 --- a/utils_private.go +++ /dev/null @@ -1,131 +0,0 @@ -package gorm - -import ( - "database/sql/driver" - "fmt" - "reflect" - "regexp" - "runtime" - "strings" -) - -func fileWithLineNum() string { - for i := 2; i < 15; i++ { - _, file, line, ok := runtime.Caller(i) - if ok && (!regexp.MustCompile(`jinzhu/gorm/.*.go`).MatchString(file) || regexp.MustCompile(`jinzhu/gorm/.*test.go`).MatchString(file)) { - return fmt.Sprintf("%v:%v", file, line) - } - } - return "" -} - -func isBlank(value reflect.Value) bool { - return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface()) -} - -func toSearchableMap(attrs ...interface{}) (result interface{}) { - if len(attrs) > 1 { - if str, ok := attrs[0].(string); ok { - result = map[string]interface{}{str: attrs[1]} - } - } else if len(attrs) == 1 { - if attr, ok := attrs[0].(map[string]interface{}); ok { - result = attr - } - - if attr, ok := attrs[0].(interface{}); ok { - result = attr - } - } - return -} - -func convertInterfaceToMap(values interface{}) map[string]interface{} { - attrs := map[string]interface{}{} - - switch value := values.(type) { - case map[string]interface{}: - for k, v := range value { - attrs[k] = v - } - case []interface{}: - for _, v := range value { - for key, value := range convertInterfaceToMap(v) { - attrs[key] = value - } - } - case interface{}: - reflectValue := reflect.ValueOf(values) - - switch reflectValue.Kind() { - case reflect.Map: - for _, key := range reflectValue.MapKeys() { - attrs[ToDBName(key.Interface().(string))] = reflectValue.MapIndex(key).Interface() - } - default: - for _, field := range (&Scope{Value: values}).Fields() { - if !field.IsBlank && !field.IsIgnored { - attrs[field.DBName] = field.Field.Interface() - } - } - } - } - return attrs -} - -func equalAsString(a interface{}, b interface{}) bool { - return toString(a) == toString(b) -} - -func toString(str interface{}) string { - if values, ok := str.([]interface{}); ok { - var results []string - for _, value := range values { - results = append(results, toString(value)) - } - return strings.Join(results, "_") - } else if bytes, ok := str.([]byte); ok { - return string(bytes) - } else if reflectValue := reflect.Indirect(reflect.ValueOf(str)); reflectValue.IsValid() { - return fmt.Sprintf("%v", reflectValue.Interface()) - } - return "" -} - -func makeSlice(elemType reflect.Type) interface{} { - if elemType.Kind() == reflect.Slice { - elemType = elemType.Elem() - } - sliceType := reflect.SliceOf(elemType) - slice := reflect.New(sliceType) - slice.Elem().Set(reflect.MakeSlice(sliceType, 0, 0)) - return slice.Interface() -} - -func strInSlice(a string, list []string) bool { - for _, b := range list { - if b == a { - return true - } - } - return false -} - -// getValueFromFields return given fields's value -func getValueFromFields(value reflect.Value, fieldNames []string) (results []interface{}) { - // If value is a nil pointer, Indirect returns a zero Value! - // Therefor we need to check for a zero value, - // as FieldByName could panic - if indirectValue := reflect.Indirect(value); indirectValue.IsValid() { - for _, fieldName := range fieldNames { - if fieldValue := indirectValue.FieldByName(fieldName); fieldValue.IsValid() { - result := fieldValue.Interface() - if r, ok := result.(driver.Valuer); ok { - result, _ = r.Value() - } - results = append(results, result) - } - } - } - return -} From 4e8370e18ba237a4c7bdad7eccddb297a45a906c Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 15 Feb 2016 14:09:24 +0800 Subject: [PATCH 42/83] Refactor dialect --- customize_column_test.go | 2 +- ddl_errors_test.go | 3 +-- dialect.go | 22 +++++++++++----- dialect_common.go | 57 ++++++++++++++++------------------------ dialect_mssql.go | 31 +++++++++++----------- dialect_mysql.go | 9 +++++-- dialect_postgres.go | 20 ++++++-------- dialect_sqlite3.go | 20 ++++++-------- main.go | 6 ++--- migration_test.go | 16 +++++------ scope_private.go | 12 ++++----- 11 files changed, 94 insertions(+), 104 deletions(-) diff --git a/customize_column_test.go b/customize_column_test.go index 93bab2e1..177b4a5d 100644 --- a/customize_column_test.go +++ b/customize_column_test.go @@ -26,7 +26,7 @@ func TestCustomizeColumn(t *testing.T) { DB.AutoMigrate(&CustomizeColumn{}) scope := DB.NewScope(&CustomizeColumn{}) - if !scope.Dialect().HasColumn(scope, scope.TableName(), col) { + if !scope.Dialect().HasColumn(scope.TableName(), col) { t.Errorf("CustomizeColumn should have column %s", col) } diff --git a/ddl_errors_test.go b/ddl_errors_test.go index aca59553..2c31b354 100644 --- a/ddl_errors_test.go +++ b/ddl_errors_test.go @@ -17,8 +17,7 @@ func TestDdlErrors(t *testing.T) { } }() - DB.HasTable("foobarbaz") - if DB.Error == nil { + if err := DB.Find(&User{}).Error; err == nil { t.Errorf("Expected operation on closed db to produce an error, but err was nil") } } diff --git a/dialect.go b/dialect.go index 1923e66e..cce68789 100644 --- a/dialect.go +++ b/dialect.go @@ -10,6 +10,9 @@ import ( // Dialect interface contains behaviors that differ across SQL database type Dialect interface { + // SetDB set db for dialect + SetDB(db *sql.DB) + // BindVar return the placeholder for actual values in SQL statements, in many dbs it is "?", Postgres using $1 BindVar(i int) string // Quote quotes field name to avoid SQL parsing exceptions by using a reserved word as a field name @@ -18,13 +21,13 @@ type Dialect interface { DataTypeOf(field *StructField) string // HasIndex check has index or not - HasIndex(scope *Scope, tableName string, indexName string) bool + HasIndex(tableName string, indexName string) bool // RemoveIndex remove index - RemoveIndex(scope *Scope, indexName string) + RemoveIndex(tableName string, indexName string) error // HasTable check has table or not - HasTable(scope *Scope, tableName string) bool + HasTable(tableName string) bool // HasColumn check has column or not - HasColumn(scope *Scope, tableName string, columnName string) bool + HasColumn(tableName string, columnName string) bool // LimitAndOffsetSQL return generate SQL with limit and offset, as mssql has special case LimitAndOffsetSQL(limit, offset int) string @@ -36,12 +39,17 @@ type Dialect interface { var dialectsMap = map[string]Dialect{} -func newDialect(name string) Dialect { - if dialect, ok := dialectsMap[name]; ok { +func newDialect(name string, db *sql.DB) Dialect { + if value, ok := dialectsMap[name]; ok { + dialect := reflect.New(reflect.TypeOf(value).Elem()).Interface().(Dialect) + dialect.SetDB(db) return dialect } + fmt.Printf("`%v` is not officially supported, running under compatibility mode.\n", name) - return &commonDialect{} + commontDialect := &commonDialect{} + commontDialect.SetDB(db) + return commontDialect } // RegisterDialect register new dialect diff --git a/dialect_common.go b/dialect_common.go index d5a81ad6..333b0b45 100644 --- a/dialect_common.go +++ b/dialect_common.go @@ -1,18 +1,25 @@ package gorm import ( + "database/sql" "fmt" "reflect" "strings" "time" ) -type commonDialect struct{} +type commonDialect struct { + db *sql.DB +} func init() { RegisterDialect("common", &commonDialect{}) } +func (s *commonDialect) SetDB(db *sql.DB) { + s.db = db +} + func (commonDialect) BindVar(i int) string { return "$$" // ? } @@ -73,51 +80,31 @@ func (commonDialect) DataTypeOf(field *StructField) string { return fmt.Sprintf("%v %v", sqlType, additionalType) } -func (c commonDialect) HasIndex(scope *Scope, tableName string, indexName string) bool { - var ( - count int - databaseName = c.currentDatabase(scope) - ) - c.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE table_schema = ? AND table_name = ? AND index_name = ?", databaseName, tableName, indexName) +func (s commonDialect) HasIndex(tableName string, indexName string) bool { + var count int + s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE table_schema = ? AND table_name = ? AND index_name = ?", s.currentDatabase(), tableName, indexName).Scan(&count) return count > 0 } -func (commonDialect) RemoveIndex(scope *Scope, indexName string) { - scope.Err(scope.NewDB().Exec(fmt.Sprintf("DROP INDEX %v ON %v", indexName, scope.QuotedTableName())).Error) +func (s commonDialect) RemoveIndex(tableName string, indexName string) error { + _, err := s.db.Exec(fmt.Sprintf("DROP INDEX %v", indexName)) + return err } -func (c commonDialect) HasTable(scope *Scope, tableName string) bool { - var ( - count int - databaseName = c.currentDatabase(scope) - ) - c.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = ? AND table_name = ?", databaseName, tableName) +func (s commonDialect) HasTable(tableName string) bool { + var count int + s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = ? AND table_name = ?", s.currentDatabase(), tableName).Scan(&count) return count > 0 } -func (c commonDialect) HasColumn(scope *Scope, tableName string, columnName string) bool { - var ( - count int - databaseName = c.currentDatabase(scope) - ) - c.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = ? AND table_name = ? AND column_name = ?", databaseName, tableName, columnName) +func (s commonDialect) HasColumn(tableName string, columnName string) bool { + var count int + s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = ? AND table_name = ? AND column_name = ?", s.currentDatabase(), tableName, columnName).Scan(&count) return count > 0 } -// RawScanInt scans the first column of the first row into the `scan' int pointer. -// This function captures raw query errors and propagates them to the original scope. -func (commonDialect) RawScanInt(scope *Scope, scanPtr *int, query string, args ...interface{}) { - scope.Err(scope.NewDB().Raw(query, args...).Row().Scan(scanPtr)) -} - -// RawScanString scans the first column of the first row into the `scan' string pointer. -// This function captures raw query errors and propagates them to the original scope. -func (commonDialect) RawScanString(scope *Scope, scanPtr *string, query string, args ...interface{}) { - scope.Err(scope.NewDB().Raw(query, args...).Row().Scan(scanPtr)) -} - -func (commonDialect) currentDatabase(scope *Scope) (name string) { - scope.Err(scope.NewDB().Raw("SELECT DATABASE()").Row().Scan(&name)) +func (s commonDialect) currentDatabase() (name string) { + s.db.QueryRow("SELECT DATABASE()").Scan(&name) return } diff --git a/dialect_mssql.go b/dialect_mssql.go index a2af49ad..63b46e9e 100644 --- a/dialect_mssql.go +++ b/dialect_mssql.go @@ -67,32 +67,31 @@ func (mssql) DataTypeOf(field *StructField) string { return fmt.Sprintf("%v %v", sqlType, additionalType) } -func (s mssql) HasIndex(scope *Scope, tableName string, indexName string) bool { +func (s mssql) HasIndex(tableName string, indexName string) bool { var count int - s.RawScanInt(scope, &count, "SELECT count(*) FROM sys.indexes WHERE name=? AND object_id=OBJECT_ID(?)", indexName, tableName) + s.db.QueryRow("SELECT count(*) FROM sys.indexes WHERE name=? AND object_id=OBJECT_ID(?)", indexName, tableName).Scan(&count) return count > 0 } -func (s mssql) HasTable(scope *Scope, tableName string) bool { - var ( - count int - databaseName = s.currentDatabase(scope) - ) - s.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.tables WHERE table_name = ? AND table_catalog = ?", tableName, databaseName) +func (s mssql) RemoveIndex(tableName string, indexName string) error { + _, err := s.db.Exec(fmt.Sprintf("DROP INDEX %v ON %v", indexName, s.Quote(tableName))) + return err +} + +func (s mssql) HasTable(tableName string) bool { + var count int + s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.tables WHERE table_name = ? AND table_catalog = ?", tableName, s.currentDatabase()).Scan(&count) return count > 0 } -func (s mssql) HasColumn(scope *Scope, tableName string, columnName string) bool { - var ( - count int - databaseName = s.currentDatabase(scope) - ) - s.RawScanInt(scope, &count, "SELECT count(*) FROM information_schema.columns WHERE table_catalog = ? AND table_name = ? AND column_name = ?", databaseName, tableName, columnName) +func (s mssql) HasColumn(tableName string, columnName string) bool { + var count int + s.db.QueryRow("SELECT count(*) FROM information_schema.columns WHERE table_catalog = ? AND table_name = ? AND column_name = ?", s.currentDatabase(), tableName, columnName).Scan(&count) return count > 0 } -func (s mssql) currentDatabase(scope *Scope) (name string) { - s.RawScanString(scope, &name, "SELECT DB_NAME() AS [Current Database]") +func (s mssql) currentDatabase() (name string) { + s.db.QueryRow("SELECT DB_NAME() AS [Current Database]").Scan(&name) return } diff --git a/dialect_mysql.go b/dialect_mysql.go index 10d63db2..9e530a9a 100644 --- a/dialect_mysql.go +++ b/dialect_mysql.go @@ -88,8 +88,13 @@ func (mysql) DataTypeOf(field *StructField) string { return fmt.Sprintf("%v %v", sqlType, additionalType) } -func (s mysql) currentDatabase(scope *Scope) (name string) { - s.RawScanString(scope, &name, "SELECT DATABASE()") +func (s mysql) RemoveIndex(tableName string, indexName string) error { + _, err := s.db.Exec(fmt.Sprintf("DROP INDEX %v ON %v", indexName, s.Quote(tableName))) + return err +} + +func (s mysql) currentDatabase() (name string) { + s.db.QueryRow("SELECT DATABASE()").Scan(&name) return } diff --git a/dialect_postgres.go b/dialect_postgres.go index e726d233..3c18acc2 100644 --- a/dialect_postgres.go +++ b/dialect_postgres.go @@ -77,30 +77,26 @@ func (postgres) DataTypeOf(field *StructField) string { return fmt.Sprintf("%v %v", sqlType, additionalType) } -func (s postgres) HasIndex(scope *Scope, tableName string, indexName string) bool { +func (s postgres) HasIndex(tableName string, indexName string) bool { var count int - s.RawScanInt(scope, &count, "SELECT count(*) FROM pg_indexes WHERE tablename = ? AND indexname = ?", tableName, indexName) + s.db.QueryRow("SELECT count(*) FROM pg_indexes WHERE tablename = $1 AND indexname = $2", tableName, indexName).Scan(&count) return count > 0 } -func (postgres) RemoveIndex(scope *Scope, indexName string) { - scope.Err(scope.NewDB().Exec(fmt.Sprintf("DROP INDEX %v", indexName)).Error) -} - -func (s postgres) HasTable(scope *Scope, tableName string) bool { +func (s postgres) HasTable(tableName string) bool { var count int - s.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.tables WHERE table_name = ? AND table_type = 'BASE TABLE'", tableName) + s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.tables WHERE table_name = $1 AND table_type = 'BASE TABLE'", tableName).Scan(&count) return count > 0 } -func (s postgres) HasColumn(scope *Scope, tableName string, columnName string) bool { +func (s postgres) HasColumn(tableName string, columnName string) bool { var count int - s.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.columns WHERE table_name = ? AND column_name = ?", tableName, columnName) + s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.columns WHERE table_name = $1 AND column_name = $2", tableName, columnName).Scan(&count) return count > 0 } -func (s postgres) currentDatabase(scope *Scope) (name string) { - s.RawScanString(scope, &name, "SELECT CURRENT_DATABASE()") +func (s postgres) currentDatabase() (name string) { + s.db.QueryRow("SELECT CURRENT_DATABASE()").Scan(&name) return } diff --git a/dialect_sqlite3.go b/dialect_sqlite3.go index 3abdb92e..41e45517 100644 --- a/dialect_sqlite3.go +++ b/dialect_sqlite3.go @@ -65,29 +65,25 @@ func (sqlite3) DataTypeOf(field *StructField) string { return fmt.Sprintf("%v %v", sqlType, additionalType) } -func (s sqlite3) HasIndex(scope *Scope, tableName string, indexName string) bool { +func (s sqlite3) HasIndex(tableName string, indexName string) bool { var count int - s.RawScanInt(scope, &count, fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE tbl_name = ? AND sql LIKE '%%INDEX %v ON%%'", indexName), tableName) + s.db.QueryRow(fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE tbl_name = ? AND sql LIKE '%%INDEX %v ON%%'", indexName), tableName).Scan(&count) return count > 0 } -func (sqlite3) RemoveIndex(scope *Scope, indexName string) { - scope.Err(scope.NewDB().Exec(fmt.Sprintf("DROP INDEX %v", indexName)).Error) -} - -func (s sqlite3) HasTable(scope *Scope, tableName string) bool { +func (s sqlite3) HasTable(tableName string) bool { var count int - s.RawScanInt(scope, &count, "SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?", tableName) + s.db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?", tableName).Scan(&count) return count > 0 } -func (s sqlite3) HasColumn(scope *Scope, tableName string, columnName string) bool { +func (s sqlite3) HasColumn(tableName string, columnName string) bool { var count int - s.RawScanInt(scope, &count, fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE tbl_name = ? AND (sql LIKE '%%(\"%v\" %%' OR sql LIKE '%%,\"%v\" %%' OR sql LIKE '%%, \"%v\" %%' OR sql LIKE '%%( %v %%' OR sql LIKE '%%, %v %%' OR sql LIKE '%%,%v %%');\n", columnName, columnName, columnName, columnName, columnName, columnName), tableName) + s.db.QueryRow(fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE tbl_name = ? AND (sql LIKE '%%(\"%v\" %%' OR sql LIKE '%%,\"%v\" %%' OR sql LIKE '%%, \"%v\" %%' OR sql LIKE '%%( %v %%' OR sql LIKE '%%, %v %%' OR sql LIKE '%%,%v %%');\n", columnName, columnName, columnName, columnName, columnName, columnName), tableName).Scan(&count) return count > 0 } -func (sqlite3) currentDatabase(scope *Scope) (name string) { +func (s sqlite3) currentDatabase() (name string) { var ( ifaces = make([]interface{}, 3) pointers = make([]*string, 3) @@ -96,7 +92,7 @@ func (sqlite3) currentDatabase(scope *Scope) (name string) { for i = 0; i < 3; i++ { ifaces[i] = &pointers[i] } - if err := scope.NewDB().Raw("PRAGMA database_list").Row().Scan(ifaces...); scope.Err(err) != nil { + if err := s.db.QueryRow("PRAGMA database_list").Scan(ifaces...); err != nil { return } if pointers[1] != nil { diff --git a/main.go b/main.go index 51bd9914..9581a216 100644 --- a/main.go +++ b/main.go @@ -62,7 +62,7 @@ func Open(dialect string, args ...interface{}) (*DB, error) { } db = DB{ - dialect: newDialect(dialect), + dialect: newDialect(dialect, dbSql.(*sql.DB)), logger: defaultLogger, callbacks: defaultCallback, source: source, @@ -430,7 +430,7 @@ func (s *DB) HasTable(value interface{}) bool { tableName = scope.TableName() } - has := scope.Dialect().HasTable(scope, tableName) + has := scope.Dialect().HasTable(tableName) s.AddError(scope.db.Error) return has } @@ -531,7 +531,7 @@ func (s *DB) SetJoinTableHandler(source interface{}, column string, handler Join destination := (&Scope{Value: reflect.New(field.Struct.Type).Interface()}).GetModelStruct().ModelType handler.Setup(field.Relationship, many2many, source, destination) field.Relationship.JoinTableHandler = handler - if table := handler.Table(s); scope.Dialect().HasTable(scope, table) { + if table := handler.Table(s); scope.Dialect().HasTable(table) { s.Table(table).AutoMigrate(handler) } } diff --git a/migration_test.go b/migration_test.go index 0411872e..de35c1df 100644 --- a/migration_test.go +++ b/migration_test.go @@ -31,7 +31,7 @@ func TestIndexes(t *testing.T) { } scope := DB.NewScope(&Email{}) - if !scope.Dialect().HasIndex(scope, scope.TableName(), "idx_email_email") { + if !scope.Dialect().HasIndex(scope.TableName(), "idx_email_email") { t.Errorf("Email should have index idx_email_email") } @@ -39,7 +39,7 @@ func TestIndexes(t *testing.T) { t.Errorf("Got error when tried to remove index: %+v", err) } - if scope.Dialect().HasIndex(scope, scope.TableName(), "idx_email_email") { + if scope.Dialect().HasIndex(scope.TableName(), "idx_email_email") { t.Errorf("Email's index idx_email_email should be deleted") } @@ -47,7 +47,7 @@ func TestIndexes(t *testing.T) { t.Errorf("Got error when tried to create index: %+v", err) } - if !scope.Dialect().HasIndex(scope, scope.TableName(), "idx_email_email_and_user_id") { + if !scope.Dialect().HasIndex(scope.TableName(), "idx_email_email_and_user_id") { t.Errorf("Email should have index idx_email_email_and_user_id") } @@ -55,7 +55,7 @@ func TestIndexes(t *testing.T) { t.Errorf("Got error when tried to remove index: %+v", err) } - if scope.Dialect().HasIndex(scope, scope.TableName(), "idx_email_email_and_user_id") { + if scope.Dialect().HasIndex(scope.TableName(), "idx_email_email_and_user_id") { t.Errorf("Email's index idx_email_email_and_user_id should be deleted") } @@ -63,7 +63,7 @@ func TestIndexes(t *testing.T) { t.Errorf("Got error when tried to create index: %+v", err) } - if !scope.Dialect().HasIndex(scope, scope.TableName(), "idx_email_email_and_user_id") { + if !scope.Dialect().HasIndex(scope.TableName(), "idx_email_email_and_user_id") { t.Errorf("Email should have index idx_email_email_and_user_id") } @@ -85,7 +85,7 @@ func TestIndexes(t *testing.T) { t.Errorf("Got error when tried to remove index: %+v", err) } - if scope.Dialect().HasIndex(scope, scope.TableName(), "idx_email_email_and_user_id") { + if scope.Dialect().HasIndex(scope.TableName(), "idx_email_email_and_user_id") { t.Errorf("Email's index idx_email_email_and_user_id should be deleted") } @@ -117,11 +117,11 @@ func TestAutoMigration(t *testing.T) { DB.Save(&BigEmail{Email: "jinzhu@example.org", UserAgent: "pc", RegisteredAt: time.Now()}) scope := DB.NewScope(&BigEmail{}) - if !scope.Dialect().HasIndex(scope, scope.TableName(), "idx_email_agent") { + if !scope.Dialect().HasIndex(scope.TableName(), "idx_email_agent") { t.Errorf("Failed to create index") } - if !scope.Dialect().HasIndex(scope, scope.TableName(), "uix_emails_registered_at") { + if !scope.Dialect().HasIndex(scope.TableName(), "uix_emails_registered_at") { t.Errorf("Failed to create index") } diff --git a/scope_private.go b/scope_private.go index 8e148820..4ed2060c 100644 --- a/scope_private.go +++ b/scope_private.go @@ -515,7 +515,7 @@ func (scope *Scope) createJoinTable(field *StructField) { if relationship := field.Relationship; relationship != nil && relationship.JoinTableHandler != nil { joinTableHandler := relationship.JoinTableHandler joinTable := joinTableHandler.Table(scope.db) - if !scope.Dialect().HasTable(scope, joinTable) { + if !scope.Dialect().HasTable(joinTable) { toScope := &Scope{Value: reflect.New(field.Struct.Type).Interface()} var sqlTypes, primaryKeys []string @@ -586,7 +586,7 @@ func (scope *Scope) dropTable() *Scope { } func (scope *Scope) dropTableIfExists() *Scope { - if scope.Dialect().HasTable(scope, scope.TableName()) { + if scope.Dialect().HasTable(scope.TableName()) { scope.dropTable() } return scope @@ -601,7 +601,7 @@ func (scope *Scope) dropColumn(column string) { } func (scope *Scope) addIndex(unique bool, indexName string, column ...string) { - if scope.Dialect().HasIndex(scope, scope.TableName(), indexName) { + if scope.Dialect().HasIndex(scope.TableName(), indexName) { return } @@ -626,18 +626,18 @@ func (scope *Scope) addForeignKey(field string, dest string, onDelete string, on } func (scope *Scope) removeIndex(indexName string) { - scope.Dialect().RemoveIndex(scope, indexName) + scope.Dialect().RemoveIndex(scope.TableName(), indexName) } func (scope *Scope) autoMigrate() *Scope { tableName := scope.TableName() quotedTableName := scope.QuotedTableName() - if !scope.Dialect().HasTable(scope, tableName) { + if !scope.Dialect().HasTable(tableName) { scope.createTable() } else { for _, field := range scope.GetModelStruct().StructFields { - if !scope.Dialect().HasColumn(scope, tableName, field.DBName) { + if !scope.Dialect().HasColumn(tableName, field.DBName) { if field.IsNormal { sqlTag := scope.Dialect().DataTypeOf(field) scope.Raw(fmt.Sprintf("ALTER TABLE %v ADD %v %v;", quotedTableName, scope.Quote(field.DBName), sqlTag)).Exec() From 5d4aae2d116f7031dd1819ec83424a5e236bc9b5 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 15 Feb 2016 16:23:26 +0800 Subject: [PATCH 43/83] Don't sort processors that without name --- callback.go | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/callback.go b/callback.go index 309078e4..3335f1db 100644 --- a/callback.go +++ b/callback.go @@ -128,9 +128,9 @@ func (cp *CallbackProcessor) Replace(callbackName string, callback func(scope *S // Get registered callback // db.Callback().Create().Get("gorm:create") func (cp *CallbackProcessor) Get(callbackName string) (callback func(scope *Scope)) { - for _, processor := range cp.parent.processors { - if processor.name == callbackName && processor.kind == cp.kind && !cp.remove { - return *cp.processor + for _, p := range cp.parent.processors { + if p.name == callbackName && p.kind == cp.kind && !cp.remove { + return *p.processor } } return nil @@ -215,17 +215,19 @@ func (c *Callback) reorder() { var creates, updates, deletes, queries, rowQueries []*CallbackProcessor for _, processor := range c.processors { - switch processor.kind { - case "create": - creates = append(creates, processor) - case "update": - updates = append(updates, processor) - case "delete": - deletes = append(deletes, processor) - case "query": - queries = append(queries, processor) - case "row_query": - rowQueries = append(rowQueries, processor) + if processor.name != "" { + switch processor.kind { + case "create": + creates = append(creates, processor) + case "update": + updates = append(updates, processor) + case "delete": + deletes = append(deletes, processor) + case "query": + queries = append(queries, processor) + case "row_query": + rowQueries = append(rowQueries, processor) + } } } From 9caf48035df8229b1e2f6c16ea98370c8ea87f67 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 15 Feb 2016 16:55:47 +0800 Subject: [PATCH 44/83] Refactor callback --- callback.go | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/callback.go b/callback.go index 3335f1db..5c7b2417 100644 --- a/callback.go +++ b/callback.go @@ -35,12 +35,6 @@ type CallbackProcessor struct { parent *Callback } -func (c *Callback) addProcessor(kind string) *CallbackProcessor { - cp := &CallbackProcessor{kind: kind, parent: c} - c.processors = append(c.processors, cp) - return cp -} - func (c *Callback) clone() *Callback { return &Callback{ creates: c.creates, @@ -60,28 +54,28 @@ func (c *Callback) clone() *Callback { // scope.Err(errors.New("error")) // }) func (c *Callback) Create() *CallbackProcessor { - return c.addProcessor("create") + return &CallbackProcessor{kind: "create", parent: c} } // Update could be used to register callbacks for updating object, refer `Create` for usage func (c *Callback) Update() *CallbackProcessor { - return c.addProcessor("update") + return &CallbackProcessor{kind: "update", parent: c} } // Delete could be used to register callbacks for deleting object, refer `Create` for usage func (c *Callback) Delete() *CallbackProcessor { - return c.addProcessor("delete") + return &CallbackProcessor{kind: "delete", parent: c} } // Query could be used to register callbacks for querying objects with query methods like `Find`, `First`, `Related`, `Association`... // refer `Create` for usage func (c *Callback) Query() *CallbackProcessor { - return c.addProcessor("query") + return &CallbackProcessor{kind: "query", parent: c} } // RowQuery could be used to register callbacks for querying objects with `Row`, `Rows`, refer `Create` for usage func (c *Callback) RowQuery() *CallbackProcessor { - return c.addProcessor("row_query") + return &CallbackProcessor{kind: "row_query", parent: c} } // After insert a new callback after callback `callbackName`, refer `Callbacks.Create` @@ -101,6 +95,7 @@ func (cp *CallbackProcessor) Register(callbackName string, callback func(scope * cp.name = callbackName cp.processor = &callback cp.parent.reorder() + cp.parent.processors = append(cp.parent.processors, cp) } // Remove a registered callback @@ -110,6 +105,7 @@ func (cp *CallbackProcessor) Remove(callbackName string) { cp.name = callbackName cp.remove = true cp.parent.reorder() + cp.parent.processors = append(cp.parent.processors, cp) } // Replace a registered callback with new callback @@ -123,6 +119,7 @@ func (cp *CallbackProcessor) Replace(callbackName string, callback func(scope *S cp.processor = &callback cp.replace = true cp.parent.reorder() + cp.parent.processors = append(cp.parent.processors, cp) } // Get registered callback From 226c00b4a8d7ac856a951e29fa6b6777c0765194 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 15 Feb 2016 17:00:28 +0800 Subject: [PATCH 45/83] Fix LimitAndOffset for Update --- dialect_common.go | 12 +++++++----- dialect_mssql.go | 18 ++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/dialect_common.go b/dialect_common.go index 333b0b45..a1c8ff5c 100644 --- a/dialect_common.go +++ b/dialect_common.go @@ -109,11 +109,13 @@ func (s commonDialect) currentDatabase() (name string) { } func (commonDialect) LimitAndOffsetSQL(limit, offset int) (sql string) { - if limit >= 0 { - sql += fmt.Sprintf(" LIMIT %d", limit) - } - if offset >= 0 { - sql += fmt.Sprintf(" OFFSET %d", offset) + if limit > 0 || offset > 0 { + if limit >= 0 { + sql += fmt.Sprintf(" LIMIT %d", limit) + } + if offset >= 0 { + sql += fmt.Sprintf(" OFFSET %d", offset) + } } return } diff --git a/dialect_mssql.go b/dialect_mssql.go index 63b46e9e..2ecc27cc 100644 --- a/dialect_mssql.go +++ b/dialect_mssql.go @@ -96,18 +96,16 @@ func (s mssql) currentDatabase() (name string) { } func (mssql) LimitAndOffsetSQL(limit, offset int) (sql string) { - if limit < 0 && offset < 0 { - return - } + if limit > 0 || offset > 0 { + if offset < 0 { + offset = 0 + } - if offset < 0 { - offset = 0 - } + sql += fmt.Sprintf(" OFFSET %d ROWS", offset) - sql += fmt.Sprintf(" OFFSET %d ROWS", offset) - - if limit >= 0 { - sql += fmt.Sprintf(" FETCH NEXT %d ROWS ONLY", limit) + if limit >= 0 { + sql += fmt.Sprintf(" FETCH NEXT %d ROWS ONLY", limit) + } } return } From f9c6d17b2aa819ca612e57d333b1b9d415fa7b5a Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 15 Feb 2016 17:22:29 +0800 Subject: [PATCH 46/83] Reorder callbacks after append it --- callback.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/callback.go b/callback.go index 5c7b2417..d2f0cf04 100644 --- a/callback.go +++ b/callback.go @@ -94,8 +94,8 @@ func (cp *CallbackProcessor) Before(callbackName string) *CallbackProcessor { func (cp *CallbackProcessor) Register(callbackName string, callback func(scope *Scope)) { cp.name = callbackName cp.processor = &callback - cp.parent.reorder() cp.parent.processors = append(cp.parent.processors, cp) + cp.parent.reorder() } // Remove a registered callback @@ -104,8 +104,8 @@ func (cp *CallbackProcessor) Remove(callbackName string) { fmt.Printf("[info] removing callback `%v` from %v\n", callbackName, fileWithLineNum()) cp.name = callbackName cp.remove = true - cp.parent.reorder() cp.parent.processors = append(cp.parent.processors, cp) + cp.parent.reorder() } // Replace a registered callback with new callback @@ -118,8 +118,8 @@ func (cp *CallbackProcessor) Replace(callbackName string, callback func(scope *S cp.name = callbackName cp.processor = &callback cp.replace = true - cp.parent.reorder() cp.parent.processors = append(cp.parent.processors, cp) + cp.parent.reorder() } // Get registered callback From c9dfd80959ef31311810835abeaaf5d07131089c Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 15 Feb 2016 19:36:48 +0800 Subject: [PATCH 47/83] Support extra options for inserting, querying, deleting, updating SQL, close #721, #769 --- README.md | 17 +++++++++++++++++ callback_create.go | 17 ++++++++++++++--- callback_delete.go | 25 ++++++++++++++++++------- callback_query.go | 7 ++++++- callback_update.go | 11 ++++++++++- utils.go | 7 +++++++ 6 files changed, 72 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9535015e..5751964f 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,11 @@ db.Create(&user) //// INSERT INTO "languages" ("name") VALUES ('EN'); //// INSERT INTO user_languages ("user_id","language_id") VALUES (111, 2); //// COMMIT; + + +// Add extra SQL option for inserting SQL +db.Set("gorm:insert_option", "ON CONFLICT").Create(&product) +// INSERT INTO products (name, code) VALUES ("name", "code") ON CONFLICT; ``` Refer [Associations](#associations) for more details @@ -281,6 +286,10 @@ db.Find(&users) // Get record with primary key db.First(&user, 10) //// SELECT * FROM users WHERE id = 10; + +// Add extra SQL option for selecting SQL +db.Set("gorm:query_option", "FOR UPDATE").First(&user, 10) +//// SELECT * FROM users WHERE id = 10 FOR UPDATE; ``` ### Query With Where (Plain SQL) @@ -460,6 +469,10 @@ db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "acti // Update multiple attributes if they are changed (update with struct only works with none zero values) db.Model(&user).Updates(User{Name: "hello", Age: 18}) //// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111; + +// Add extra SQL option for updating SQL +db.Model(&user).Set("gorm:update_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Update("name, "hello") +//// UPDATE users SET name='hello', updated_at = '2013-11-17 21:34:10' WHERE id=111 OPTION (OPTIMIZE FOR UNKNOWN); ``` ### Update Without Callbacks @@ -513,6 +526,10 @@ DB.Model(&product).Where("quantity > 1").UpdateColumn("quantity", gorm.Expr("qua // Delete an existing record db.Delete(&email) //// DELETE from emails where id=10; + +// Add extra SQL option for deleting SQL +db.Set("gorm:delete_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Delete(&email) +//// DELETE from emails where id=10 OPTION (OPTIMIZE FOR UNKNOWN); ``` ### Batch Delete diff --git a/callback_create.go b/callback_create.go index 0ba3feac..6316f9ee 100644 --- a/callback_create.go +++ b/callback_create.go @@ -75,8 +75,13 @@ func createCallback(scope *Scope) { returningColumn = "*" quotedTableName = scope.QuotedTableName() primaryField = scope.PrimaryField() + extraOption string ) + if str, ok := scope.Get("gorm:insert_option"); ok { + extraOption = fmt.Sprint(str) + } + if primaryField != nil { returningColumn = scope.Quote(primaryField.DBName) } @@ -84,14 +89,20 @@ func createCallback(scope *Scope) { lastInsertIdReturningSuffix := scope.Dialect().LastInsertIdReturningSuffix(quotedTableName, returningColumn) if len(columns) == 0 { - scope.Raw(fmt.Sprintf("INSERT INTO %v DEFAULT VALUES %v", quotedTableName, lastInsertIdReturningSuffix)) + scope.Raw(fmt.Sprintf( + "INSERT INTO %v DEFAULT VALUES%v%v", + quotedTableName, + addExtraSpaceIfExist(extraOption), + addExtraSpaceIfExist(lastInsertIdReturningSuffix), + )) } else { scope.Raw(fmt.Sprintf( - "INSERT INTO %v (%v) VALUES (%v) %v", + "INSERT INTO %v (%v) VALUES (%v)%v%v", scope.QuotedTableName(), strings.Join(columns, ","), strings.Join(placeholders, ","), - lastInsertIdReturningSuffix, + addExtraSpaceIfExist(extraOption), + addExtraSpaceIfExist(lastInsertIdReturningSuffix), )) } diff --git a/callback_delete.go b/callback_delete.go index b3a77926..9db0666c 100644 --- a/callback_delete.go +++ b/callback_delete.go @@ -21,15 +21,26 @@ func beforeDeleteCallback(scope *Scope) { // deleteCallback used to delete data from database or set deleted_at to current time (when using with soft delete) func deleteCallback(scope *Scope) { if !scope.HasError() { + var extraOption string + if str, ok := scope.Get("gorm:delete_option"); ok { + extraOption = fmt.Sprint(str) + } + if !scope.Search.Unscoped && scope.HasColumn("DeletedAt") { - scope.Raw( - fmt.Sprintf("UPDATE %v SET deleted_at=%v %v", - scope.QuotedTableName(), - scope.AddToVars(NowFunc()), - scope.CombinedConditionSql(), - )).Exec() + scope.Raw(fmt.Sprintf( + "UPDATE %v SET deleted_at=%v%v%v", + scope.QuotedTableName(), + scope.AddToVars(NowFunc()), + addExtraSpaceIfExist(scope.CombinedConditionSql()), + addExtraSpaceIfExist(extraOption), + )).Exec() } else { - scope.Raw(fmt.Sprintf("DELETE FROM %v %v", scope.QuotedTableName(), scope.CombinedConditionSql())).Exec() + scope.Raw(fmt.Sprintf( + "DELETE FROM %v%v%v", + scope.QuotedTableName(), + addExtraSpaceIfExist(scope.CombinedConditionSql()), + addExtraSpaceIfExist(extraOption), + )).Exec() } } } diff --git a/callback_query.go b/callback_query.go index 8067c855..0221c322 100644 --- a/callback_query.go +++ b/callback_query.go @@ -51,8 +51,13 @@ func queryCallback(scope *Scope) { scope.prepareQuerySql() if !scope.HasError() { + var extraOption string + if str, ok := scope.Get("gorm:query_option"); ok { + extraOption = fmt.Sprint(str) + } scope.db.RowsAffected = 0 - if rows, err := scope.SqlDB().Query(scope.Sql, scope.SqlVars...); scope.Err(err) == nil { + + if rows, err := scope.SqlDB().Query(scope.Sql+addExtraSpaceIfExist(extraOption), scope.SqlVars...); scope.Err(err) == nil { defer rows.Close() columns, _ := rows.Columns() diff --git a/callback_update.go b/callback_update.go index b3a6c7da..44f9a143 100644 --- a/callback_update.go +++ b/callback_update.go @@ -90,9 +90,18 @@ func updateCallback(scope *Scope) { } } + var extraOption string + if str, ok := scope.Get("gorm:update_option"); ok { + extraOption = fmt.Sprint(str) + } + if len(sqls) > 0 { scope.Raw(fmt.Sprintf( - "UPDATE %v SET %v %v", scope.QuotedTableName(), strings.Join(sqls, ", "), scope.CombinedConditionSql(), + "UPDATE %v SET %v%v%v", + scope.QuotedTableName(), + strings.Join(sqls, ", "), + addExtraSpaceIfExist(scope.CombinedConditionSql()), + addExtraSpaceIfExist(extraOption), )).Exec() } } diff --git a/utils.go b/utils.go index 94e345cc..bfdaf9f7 100644 --- a/utils.go +++ b/utils.go @@ -276,3 +276,10 @@ func getValueFromFields(value reflect.Value, fieldNames []string) (results []int } return } + +func addExtraSpaceIfExist(str string) string { + if str != "" { + return " " + str + } + return "" +} From b054f235b9bf29f2e6ba7c0fe48e41509639c5cd Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 15 Feb 2016 19:42:49 +0800 Subject: [PATCH 48/83] Fix query with extra option --- callback_query.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/callback_query.go b/callback_query.go index 0221c322..11f8b476 100644 --- a/callback_query.go +++ b/callback_query.go @@ -51,13 +51,12 @@ func queryCallback(scope *Scope) { scope.prepareQuerySql() if !scope.HasError() { - var extraOption string - if str, ok := scope.Get("gorm:query_option"); ok { - extraOption = fmt.Sprint(str) - } scope.db.RowsAffected = 0 + if str, ok := scope.Get("gorm:query_option"); ok { + scope.Sql += addExtraSpaceIfExist(fmt.Sprint(str)) + } - if rows, err := scope.SqlDB().Query(scope.Sql+addExtraSpaceIfExist(extraOption), scope.SqlVars...); scope.Err(err) == nil { + if rows, err := scope.SqlDB().Query(scope.Sql, scope.SqlVars...); scope.Err(err) == nil { defer rows.Close() columns, _ := rows.Columns() From 5883c7047894b59a65c79930343e56fdb7436777 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 15 Feb 2016 21:29:47 +0800 Subject: [PATCH 49/83] Support custom preloading SQL, close #598, #793, #824 --- README.md | 12 ++++++++++ callback_query_preload.go | 49 +++++++++++++++++++++++++++++---------- preload_test.go | 4 +++- 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 5751964f..0f195561 100644 --- a/README.md +++ b/README.md @@ -432,6 +432,18 @@ db.Preload("Orders").Preload("Profile").Preload("Role").Find(&users) //// SELECT * FROM roles WHERE id IN (4,5,6); // belongs to ``` +#### Custom Preloading SQL + +You could custom preloading SQL by passing in `func(db *gorm.DB) *gorm.DB` (same type as the one used for [Scopes](#scopes)), for example: + +```go +db.Preload("Orders", func(db *gorm.DB) *gorm.DB { + return db.Order("orders.amount DESC") +}).Find(&users) +//// SELECT * FROM users; +//// SELECT * FROM orders WHERE user_id IN (1,2,3,4) order by orders.amount DESC; +``` + #### Nested Preloading ```go diff --git a/callback_query_preload.go b/callback_query_preload.go index 97591915..e57caad0 100644 --- a/callback_query_preload.go +++ b/callback_query_preload.go @@ -73,6 +73,23 @@ func preloadCallback(scope *Scope) { } } +func (scope *Scope) generatePreloadDBWithConditions(conditions []interface{}) (*DB, []interface{}) { + var ( + preloadDB = scope.NewDB() + preloadConditions []interface{} + ) + + for _, condition := range conditions { + if scopes, ok := condition.(func(*DB) *DB); ok { + preloadDB = scopes(preloadDB) + } else { + preloadConditions = append(preloadConditions, condition) + } + } + + return preloadDB, preloadConditions +} + // handleHasOnePreload used to preload has one associations func (scope *Scope) handleHasOnePreload(field *Field, conditions []interface{}) { relation := field.Relationship @@ -83,9 +100,12 @@ func (scope *Scope) handleHasOnePreload(field *Field, conditions []interface{}) return } + // preload conditions + preloadDB, preloadConditions := scope.generatePreloadDBWithConditions(conditions) + // find relations results := makeSlice(field.Struct.Type) - scope.Err(scope.NewDB().Where(fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relation.ForeignDBNames), toQueryMarks(primaryKeys)), toQueryValues(primaryKeys)...).Find(results, conditions...).Error) + scope.Err(preloadDB.Where(fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relation.ForeignDBNames), toQueryMarks(primaryKeys)), toQueryValues(primaryKeys)...).Find(results, preloadConditions...).Error) // assign find results var ( @@ -119,9 +139,12 @@ func (scope *Scope) handleHasManyPreload(field *Field, conditions []interface{}) return } + // preload conditions + preloadDB, preloadConditions := scope.generatePreloadDBWithConditions(conditions) + // find relations results := makeSlice(field.Struct.Type) - scope.Err(scope.NewDB().Where(fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relation.ForeignDBNames), toQueryMarks(primaryKeys)), toQueryValues(primaryKeys)...).Find(results, conditions...).Error) + scope.Err(preloadDB.Where(fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relation.ForeignDBNames), toQueryMarks(primaryKeys)), toQueryValues(primaryKeys)...).Find(results, preloadConditions...).Error) // assign find results var ( @@ -151,6 +174,9 @@ func (scope *Scope) handleHasManyPreload(field *Field, conditions []interface{}) func (scope *Scope) handleBelongsToPreload(field *Field, conditions []interface{}) { relation := field.Relationship + // preload conditions + preloadDB, preloadConditions := scope.generatePreloadDBWithConditions(conditions) + // get relations's primary keys primaryKeys := scope.getColumnAsArray(relation.ForeignFieldNames, scope.Value) if len(primaryKeys) == 0 { @@ -159,7 +185,7 @@ func (scope *Scope) handleBelongsToPreload(field *Field, conditions []interface{ // find relations results := makeSlice(field.Struct.Type) - scope.Err(scope.NewDB().Where(fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relation.AssociationForeignDBNames), toQueryMarks(primaryKeys)), toQueryValues(primaryKeys)...).Find(results, conditions...).Error) + scope.Err(preloadDB.Where(fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relation.AssociationForeignDBNames), toQueryMarks(primaryKeys)), toQueryValues(primaryKeys)...).Find(results, preloadConditions...).Error) // assign find results var ( @@ -205,21 +231,20 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface sourceKeys = append(sourceKeys, key.DBName) } + // preload conditions + preloadDB, preloadConditions := scope.generatePreloadDBWithConditions(conditions) + // generate query with join table newScope := scope.New(reflect.New(fieldType).Interface()) - preloadJoinDB := scope.NewDB().Table(newScope.TableName()).Select("*") - preloadJoinDB = joinTableHandler.JoinWith(joinTableHandler, preloadJoinDB, scope.Value) - - if primaryField := newScope.PrimaryField(); primaryField != nil { - preloadJoinDB = preloadJoinDB.Order(fmt.Sprintf("%v.%v %v", newScope.QuotedTableName(), newScope.Quote(primaryField.DBName), "ASC")) - } + preloadDB = preloadDB.Table(newScope.TableName()).Select("*") + preloadDB = joinTableHandler.JoinWith(joinTableHandler, preloadDB, scope.Value) // preload inline conditions - if len(conditions) > 0 { - preloadJoinDB = preloadJoinDB.Where(conditions[0], conditions[1:]...) + if len(preloadConditions) > 0 { + preloadDB = preloadDB.Where(preloadConditions[0], preloadConditions[1:]...) } - rows, err := preloadJoinDB.Rows() + rows, err := preloadDB.Rows() if scope.Err(err) != nil { return diff --git a/preload_test.go b/preload_test.go index 3ba0cf92..8f21bc97 100644 --- a/preload_test.go +++ b/preload_test.go @@ -1107,7 +1107,9 @@ func TestNestedManyToManyPreload3(t *testing.T) { } var gots []*Level3 - if err := DB.Preload("Level2.Level1s").Find(&gots).Error; err != nil { + if err := DB.Preload("Level2.Level1s", func(db *gorm.DB) *gorm.DB { + return db.Order("level1.id ASC") + }).Find(&gots).Error; err != nil { t.Error(err) } From 115789960a57e1909cf29c5fba5018e071a48965 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Tue, 16 Feb 2016 22:48:26 +0800 Subject: [PATCH 50/83] Joins support parameters, close #673 --- README.md | 2 +- main.go | 4 ++-- main_test.go | 14 ++++++++++---- scope_private.go | 14 ++++++++++---- search.go | 6 +++--- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 0f195561..1c06820e 100644 --- a/README.md +++ b/README.md @@ -929,7 +929,7 @@ db.Table("users").Select("users.name, emails.email").Joins("left join emails on db.Joins("inner join emails on emails.user_id = users.id").Where("emails.email = ?", "x@example.org").Find(&user) // find all email addresses for a user -db.Joins("left join users on users.id = emails.user_id").Where("users.name = ?", "jinzhu").Find(&emails) +db.Joins("LEFT JOIN users ON users.id = emails.user_id AND users.name = ?", "jinzhu").Find(&emails) ``` ## Transactions diff --git a/main.go b/main.go index 9581a216..b7f0d2aa 100644 --- a/main.go +++ b/main.go @@ -171,8 +171,8 @@ func (s *DB) Having(query string, values ...interface{}) *DB { return s.clone().search.Having(query, values...).db } -func (s *DB) Joins(query string) *DB { - return s.clone().search.Joins(query).db +func (s *DB) Joins(query string, args ...interface{}) *DB { + return s.clone().search.Joins(query, args...).db } func (s *DB) Scopes(funcs ...func(*DB) *DB) *DB { diff --git a/main_test.go b/main_test.go index 97a3d84e..39189cd3 100644 --- a/main_test.go +++ b/main_test.go @@ -506,10 +506,16 @@ func TestJoins(t *testing.T) { } DB.Save(&user) - var result User - DB.Joins("left join emails on emails.user_id = users.id").Where("name = ?", "joins").First(&result) - if result.Name != "joins" || result.Id != user.Id { - t.Errorf("Should find all two emails with Join") + var users1 []User + DB.Joins("left join emails on emails.user_id = users.id").Where("name = ?", "joins").Find(&users1) + if len(users1) != 2 { + t.Errorf("should find two users using left join") + } + + var users2 []User + DB.Joins("left join emails on emails.user_id = users.id AND emails.email = ?", "join1@example.com").Where("name = ?", "joins").First(&users2) + if len(users2) != 1 { + t.Errorf("should find one users using left join with conditions") } } diff --git a/scope_private.go b/scope_private.go index 4ed2060c..6b34a4b3 100644 --- a/scope_private.go +++ b/scope_private.go @@ -234,7 +234,7 @@ var hasCountRegexp = regexp.MustCompile(`(?i)count\(.+\)`) func (scope *Scope) selectSql() string { if len(scope.Search.selects) == 0 { - if scope.Search.joins != "" { + if len(scope.Search.joinConditions) > 0 { return fmt.Sprintf("%v.*", scope.QuotedTableName()) } return "*" @@ -263,12 +263,11 @@ func (scope *Scope) groupSql() string { } func (scope *Scope) havingSql() string { - if scope.Search.havingConditions == nil { + if len(scope.Search.havingConditions) == 0 { return "" } var andConditions []string - for _, clause := range scope.Search.havingConditions { if sql := scope.buildWhereCondition(clause); sql != "" { andConditions = append(andConditions, sql) @@ -284,7 +283,14 @@ func (scope *Scope) havingSql() string { } func (scope *Scope) joinsSql() string { - return scope.Search.joins + " " + var joinConditions []string + for _, clause := range scope.Search.joinConditions { + if sql := scope.buildWhereCondition(clause); sql != "" { + joinConditions = append(joinConditions, strings.TrimSuffix(strings.TrimPrefix(sql, "("), ")")) + } + } + + return strings.Join(joinConditions, " ") + " " } func (scope *Scope) prepareQuerySql() { diff --git a/search.go b/search.go index c6d070f0..4e31ae03 100644 --- a/search.go +++ b/search.go @@ -8,12 +8,12 @@ type search struct { orConditions []map[string]interface{} notConditions []map[string]interface{} havingConditions []map[string]interface{} + joinConditions []map[string]interface{} initAttrs []interface{} assignAttrs []interface{} selects map[string]interface{} omits []string orders []string - joins string preload []searchPreload offset int limit int @@ -102,8 +102,8 @@ func (s *search) Having(query string, values ...interface{}) *search { return s } -func (s *search) Joins(query string) *search { - s.joins = query +func (s *search) Joins(query string, values ...interface{}) *search { + s.joinConditions = append(s.joinConditions, map[string]interface{}{"query": query, "args": values}) return s } From 52ae6df6fd14fcc81ff215d79a13d8effab5ba39 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Thu, 18 Feb 2016 20:51:49 +0800 Subject: [PATCH 51/83] Test Updates with blank values --- update_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/update_test.go b/update_test.go index c3801c37..417463bb 100644 --- a/update_test.go +++ b/update_test.go @@ -420,6 +420,22 @@ func TestUpdateColumnsSkipsAssociations(t *testing.T) { } } +func TestUpdatesWithBlankValues(t *testing.T) { + t.Skip("not implemented") + + product := Product{Code: "product1", Price: 10} + DB.Save(&product) + + DB.Model(&Product{Id: product.Id}).Updates(&Product{Price: 100}) + + var product1 Product + DB.First(&product1, product.Id) + + if product1.Code != "product1" || product1.Price != 100 { + t.Errorf("product's code should not be updated") + } +} + func TestUpdateDecodeVirtualAttributes(t *testing.T) { t.Skip("not implemented") From 6bd0862811b1a7308bba68ec3f80053212f0b5d8 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Thu, 18 Feb 2016 22:24:35 +0800 Subject: [PATCH 52/83] Only update non blank fields that has been changed --- association.go | 2 +- callback_update.go | 21 ++++----------------- main.go | 2 +- scope.go | 11 ++++++++++- scope_private.go | 42 +++++++++++++++++------------------------- update_test.go | 9 +++++---- utils.go | 4 +--- 7 files changed, 39 insertions(+), 52 deletions(-) diff --git a/association.go b/association.go index d1984229..2df571f5 100644 --- a/association.go +++ b/association.go @@ -177,7 +177,7 @@ func (association *Association) Delete(values ...interface{}) *Association { modelValue := reflect.New(scope.GetModelStruct().ModelType).Interface() if results := newDB.Model(modelValue).UpdateColumn(foreignKeyMap); results.Error == nil { if results.RowsAffected > 0 { - scope.updatedAttrsWithValues(foreignKeyMap, false) + scope.updatedAttrsWithValues(foreignKeyMap) } } else { association.setErr(results.Error) diff --git a/callback_update.go b/callback_update.go index 44f9a143..b71a47b4 100644 --- a/callback_update.go +++ b/callback_update.go @@ -22,17 +22,10 @@ func init() { func assignUpdatingAttributesCallback(scope *Scope) { if attrs, ok := scope.InstanceGet("gorm:update_interface"); ok { if maps := convertInterfaceToMap(attrs); len(maps) > 0 { - protected, ok := scope.Get("gorm:ignore_protected_attrs") - _, updateColumn := scope.Get("gorm:update_column") - updateAttrs, hasUpdate := scope.updatedAttrsWithValues(maps, ok && protected.(bool)) - - if updateColumn { - scope.InstanceSet("gorm:update_attrs", maps) - } else if len(updateAttrs) > 0 { - scope.InstanceSet("gorm:update_attrs", updateAttrs) - } else if !hasUpdate { + if updateMaps, hasUpdate := scope.updatedAttrsWithValues(maps); hasUpdate { + scope.InstanceSet("gorm:update_attrs", updateMaps) + } else { scope.SkipLeft() - return } } } @@ -64,13 +57,7 @@ func updateCallback(scope *Scope) { if updateAttrs, ok := scope.InstanceGet("gorm:update_attrs"); ok { for column, value := range updateAttrs.(map[string]interface{}) { - if field, ok := scope.FieldByName(column); ok { - if scope.changeableField(field) { - sqls = append(sqls, fmt.Sprintf("%v = %v", scope.Quote(field.DBName), scope.AddToVars(value))) - } - } else { - sqls = append(sqls, fmt.Sprintf("%v = %v", scope.Quote(column), scope.AddToVars(value))) - } + sqls = append(sqls, fmt.Sprintf("%v = %v", scope.Quote(column), scope.AddToVars(value))) } } else { fields := scope.Fields() diff --git a/main.go b/main.go index b7f0d2aa..cfa71b60 100644 --- a/main.go +++ b/main.go @@ -258,7 +258,7 @@ func (s *DB) FirstOrInit(out interface{}, where ...interface{}) *DB { } c.NewScope(out).inlineCondition(where...).initialize() } else { - c.NewScope(out).updatedAttrsWithValues(convertInterfaceToMap(c.search.assignAttrs), false) + c.NewScope(out).updatedAttrsWithValues(convertInterfaceToMap(c.search.assignAttrs)) } return c } diff --git a/scope.go b/scope.go index 6d9303ec..c84a8179 100644 --- a/scope.go +++ b/scope.go @@ -154,20 +154,29 @@ func (scope *Scope) HasColumn(column string) bool { // SetColumn to set the column's value func (scope *Scope) SetColumn(column interface{}, value interface{}) error { + var updateAttrs = map[string]interface{}{} + if attrs, ok := scope.InstanceGet("gorm:update_attrs"); ok { + updateAttrs = attrs.(map[string]interface{}) + defer scope.InstanceSet("gorm:update_attrs", updateAttrs) + } + if field, ok := column.(*Field); ok { + updateAttrs[field.DBName] = value return field.Set(value) } else if name, ok := column.(string); ok { - if field, ok := scope.Fields()[name]; ok { + updateAttrs[field.DBName] = value return field.Set(value) } dbName := ToDBName(name) if field, ok := scope.Fields()[dbName]; ok { + updateAttrs[field.DBName] = value return field.Set(value) } if field, ok := scope.FieldByName(name); ok { + updateAttrs[field.DBName] = value return field.Set(value) } } diff --git a/scope_private.go b/scope_private.go index 6b34a4b3..9b01dcb9 100644 --- a/scope_private.go +++ b/scope_private.go @@ -319,38 +319,30 @@ func (scope *Scope) callCallbacks(funcs []*func(s *Scope)) *Scope { return scope } -func (scope *Scope) updatedAttrsWithValues(values map[string]interface{}, ignoreProtectedAttrs bool) (results map[string]interface{}, hasUpdate bool) { - if !scope.IndirectValue().CanAddr() { +func (scope *Scope) updatedAttrsWithValues(values map[string]interface{}) (results map[string]interface{}, hasUpdate bool) { + if scope.IndirectValue().Kind() != reflect.Struct { return values, true } - var hasExpr bool + results = map[string]interface{}{} for key, value := range values { - if field, ok := scope.FieldByName(key); ok && field.Field.IsValid() { + if field, ok := scope.FieldByName(key); ok && scope.changeableField(field) { if !reflect.DeepEqual(field.Field, reflect.ValueOf(value)) { - if _, ok := value.(*expr); ok { - hasExpr = true - } else if !equalAsString(field.Field.Interface(), value) { - hasUpdate = true + if field.IsNormal { + if _, ok := value.(*expr); ok { + hasUpdate = true + results[field.DBName] = value + } else if !equalAsString(field.Field.Interface(), value) { + hasUpdate = true + field.Set(value) + results[field.DBName] = field.Field.Interface() + } + } else { field.Set(value) } } } } - - if hasExpr { - var updateMap = map[string]interface{}{} - for key, field := range scope.Fields() { - if field.IsNormal { - if v, ok := values[key]; ok { - updateMap[key] = v - } else { - updateMap[key] = field.Field.Interface() - } - } - } - return updateMap, true - } return } @@ -370,10 +362,10 @@ func (scope *Scope) rows() (*sql.Rows, error) { func (scope *Scope) initialize() *Scope { for _, clause := range scope.Search.whereConditions { - scope.updatedAttrsWithValues(convertInterfaceToMap(clause["query"]), false) + scope.updatedAttrsWithValues(convertInterfaceToMap(clause["query"])) } - scope.updatedAttrsWithValues(convertInterfaceToMap(scope.Search.initAttrs), false) - scope.updatedAttrsWithValues(convertInterfaceToMap(scope.Search.assignAttrs), false) + scope.updatedAttrsWithValues(convertInterfaceToMap(scope.Search.initAttrs)) + scope.updatedAttrsWithValues(convertInterfaceToMap(scope.Search.assignAttrs)) return scope } diff --git a/update_test.go b/update_test.go index 417463bb..fd193ada 100644 --- a/update_test.go +++ b/update_test.go @@ -71,13 +71,14 @@ func TestUpdate(t *testing.T) { } DB.First(&product4, product4.Id) + updatedAt4 := product4.UpdatedAt DB.Model(&product4).Update("price", gorm.Expr("price + ? - ?", 100, 50)) var product5 Product DB.First(&product5, product4.Id) if product5.Price != product4.Price+100-50 { t.Errorf("Update with expression") } - if product5.UpdatedAt.Format(time.RFC3339Nano) == product4.UpdatedAt.Format(time.RFC3339Nano) { + if product4.UpdatedAt.Format(time.RFC3339Nano) == updatedAt4.Format(time.RFC3339Nano) { t.Errorf("Update with expression should update UpdatedAt") } } @@ -170,13 +171,15 @@ func TestUpdates(t *testing.T) { t.Errorf("product2's code should be updated") } + updatedAt4 := product4.UpdatedAt DB.Model(&product4).Updates(map[string]interface{}{"price": gorm.Expr("price + ?", 100)}) var product5 Product DB.First(&product5, product4.Id) if product5.Price != product4.Price+100 { t.Errorf("Updates with expression") } - if product5.UpdatedAt.Format(time.RFC3339Nano) == product4.UpdatedAt.Format(time.RFC3339Nano) { + // product4's UpdatedAt will be reset when updating + if product4.UpdatedAt.Format(time.RFC3339Nano) == updatedAt4.Format(time.RFC3339Nano) { t.Errorf("Updates with expression should update UpdatedAt") } } @@ -421,8 +424,6 @@ func TestUpdateColumnsSkipsAssociations(t *testing.T) { } func TestUpdatesWithBlankValues(t *testing.T) { - t.Skip("not implemented") - product := Product{Code: "product1", Price: 10} DB.Save(&product) diff --git a/utils.go b/utils.go index bfdaf9f7..55d75619 100644 --- a/utils.go +++ b/utils.go @@ -192,9 +192,7 @@ func convertInterfaceToMap(values interface{}) map[string]interface{} { switch value := values.(type) { case map[string]interface{}: - for k, v := range value { - attrs[k] = v - } + return value case []interface{}: for _, v := range value { for key, value := range convertInterfaceToMap(v) { From 2786ab34b4e19fc12ebaa148519c999fb4093f0a Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Thu, 18 Feb 2016 23:29:45 +0800 Subject: [PATCH 53/83] Support decode to virtual attributes, close #798 --- scope_private.go | 18 +++++++++--------- update_test.go | 2 -- utils.go | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/scope_private.go b/scope_private.go index 9b01dcb9..ceaaf9c6 100644 --- a/scope_private.go +++ b/scope_private.go @@ -328,18 +328,18 @@ func (scope *Scope) updatedAttrsWithValues(values map[string]interface{}) (resul for key, value := range values { if field, ok := scope.FieldByName(key); ok && scope.changeableField(field) { if !reflect.DeepEqual(field.Field, reflect.ValueOf(value)) { - if field.IsNormal { - if _, ok := value.(*expr); ok { + if _, ok := value.(*expr); ok { + hasUpdate = true + results[field.DBName] = value + } else if !equalAsString(field.Field.Interface(), value) { + field.Set(value) + if field.IsNormal { hasUpdate = true - results[field.DBName] = value - } else if !equalAsString(field.Field.Interface(), value) { - hasUpdate = true - field.Set(value) results[field.DBName] = field.Field.Interface() } - } else { - field.Set(value) } + } else { + field.Set(value) } } } @@ -428,7 +428,7 @@ func (scope *Scope) changeableField(field *Field) bool { } } - return !field.IsIgnored + return true } func (scope *Scope) shouldSaveAssociations() bool { diff --git a/update_test.go b/update_test.go index fd193ada..218c5834 100644 --- a/update_test.go +++ b/update_test.go @@ -438,8 +438,6 @@ func TestUpdatesWithBlankValues(t *testing.T) { } func TestUpdateDecodeVirtualAttributes(t *testing.T) { - t.Skip("not implemented") - var user = User{ Name: "jinzhu", IgnoreMe: 88, diff --git a/utils.go b/utils.go index 55d75619..f6bce479 100644 --- a/utils.go +++ b/utils.go @@ -209,7 +209,7 @@ func convertInterfaceToMap(values interface{}) map[string]interface{} { } default: for _, field := range (&Scope{Value: values}).Fields() { - if !field.IsBlank && !field.IsIgnored { + if !field.IsBlank { attrs[field.DBName] = field.Field.Interface() } } From e7c4e7de4e85f6f3a24da8fea0f4c785138711be Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Fri, 19 Feb 2016 11:23:08 +0800 Subject: [PATCH 54/83] Update README for Updating --- README.md | 97 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 1c06820e..4d078b87 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,14 @@ go get -u github.com/jinzhu/gorm - [Query Chains](#query-chains) - [Preloading (Eager loading)](#preloading-eager-loading) - [Update](#update) - - [Update Without Callbacks](#update-without-callbacks) + - [Update All Fields](#update-all-fields) + - [Update Changed Fields](#update-changed-fields) + - [Update Selected Fields](#update-selected-fields) + - [Update Changed Fields Without Callbacks](#update-changed-fields-without-callbacks) - [Batch Updates](#batch-updates) - [Update with SQL Expression](#update-with-sql-expression) + - [Change Updating Values In Callbacks](#change-updating-values-in-callbacks) + - [Extra Update option](#extra-update-option) - [Delete](#delete) - [Batch Delete](#batch-delete) - [Soft Delete](#soft-delete) @@ -453,66 +458,86 @@ db.Preload("Orders", "state = ?", "paid").Preload("Orders.OrderItems").Find(&use ## Update +### Update All Fields + +`Save` will include all fields when perform the Updating SQL, even it is not changed + ```go -// Update an existing struct db.First(&user) + user.Name = "jinzhu 2" user.Age = 100 db.Save(&user) -//// UPDATE users SET name='jinzhu 2', age=100, updated_at = '2013-11-17 21:34:10' WHERE id=111; -db.Where("active = ?", true).Save(&user) -//// UPDATE users SET name='jinzhu 2', age=100, updated_at = '2013-11-17 21:34:10' WHERE id=111 AND active = true; +//// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111; +``` -// Update an attribute if it is changed +### Update Changed Fields + +If you only want to update changed Fields, you could use `Update`, `Updates` + +```go +// Update single attribute if it is changed db.Model(&user).Update("name", "hello") -//// UPDATE users SET name='hello', updated_at = '2013-11-17 21:34:10' WHERE id=111; +//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111; +// Update single attribute with combined conditions db.Model(&user).Where("active = ?", true).Update("name", "hello") -//// UPDATE users SET name='hello', updated_at = '2013-11-17 21:34:10' WHERE id=111 AND active = true; +//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true; -db.First(&user, 111).Update("name", "hello") -//// SELECT * FROM users LIMIT 1; -//// UPDATE users SET name='hello', updated_at = '2013-11-17 21:34:10' WHERE id=111; -// Update multiple attributes if they are changed +// Update multiple attributes with `map`, will only update those changed fields db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) +//// UPDATE users SET name='hello', age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111; -// Update multiple attributes if they are changed (update with struct only works with none zero values) +// Update multiple attributes with `struct`, will only update those changed & non blank fields db.Model(&user).Updates(User{Name: "hello", Age: 18}) //// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111; -// Add extra SQL option for updating SQL -db.Model(&user).Set("gorm:update_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Update("name, "hello") -//// UPDATE users SET name='hello', updated_at = '2013-11-17 21:34:10' WHERE id=111 OPTION (OPTIMIZE FOR UNKNOWN); +// WARNING when update with struct, GORM will only update those fields that with non blank value +// For below Update, nothing will be updated as "", 0, false are blank values of their types +db.Model(&user).Updates(User{Name: "", Age: 0, Actived: false}) ``` -### Update Without Callbacks +### Update Selected Fields -By default, update will call BeforeUpdate, AfterUpdate callbacks, if you want to update w/o callbacks and w/o saving associations: +If you only want to update or ignore some fields when updating, you could use `Select`, `Omit` ```go +db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) +//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111; + +db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) +//// UPDATE users SET age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111; +``` + +### Update Changed Fields Without Callbacks + +Updating operations above will invoke `BeforeUpdate`, `AfterUpdate`, Update UpdatedAt timestamp, Save Associations callbacks, if you don't call them, you could use `UpdateColumn`, `UpdateColumns` + +```go +// Update single attribute, similar with `Update` db.Model(&user).UpdateColumn("name", "hello") //// UPDATE users SET name='hello' WHERE id = 111; -// Update with struct only works with none zero values, or use map[string]interface{} +// Update multiple attributes, similar with `Updates` db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18}) //// UPDATE users SET name='hello', age=18 WHERE id = 111; ``` ### Batch Updates +Callbacks won't run when do batch updates + ```go -db.Table("users").Where("id = ?", 10).Updates(map[string]interface{}{"name": "hello", "age": 18}) -//// UPDATE users SET name='hello', age=18 WHERE id = 10; +db.Table("users").Where("id IN (?)", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18}) +//// UPDATE users SET name='hello', age=18 WHERE id IN (10, 11); // Update with struct only works with none zero values, or use map[string]interface{} db.Model(User{}).Updates(User{Name: "hello", Age: 18}) //// UPDATE users SET name='hello', age=18; -// Callbacks won't run when do batch updates - -// Use `RowsAffected` to get the count of affected records +// Get updated records count with `RowsAffected` db.Model(User{}).Updates(User{Name: "hello", Age: 18}).RowsAffected ``` @@ -520,10 +545,10 @@ db.Model(User{}).Updates(User{Name: "hello", Age: 18}).RowsAffected ```go DB.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100)) -//// UPDATE "products" SET "code" = 'L1212', "price" = price * '2' + '100', "updated_at" = '2013-11-17 21:34:10' WHERE "id" = '2'; +//// UPDATE "products" SET "price" = price * '2' + '100', "updated_at" = '2013-11-17 21:34:10' WHERE "id" = '2'; DB.Model(&product).Updates(map[string]interface{}{"price": gorm.Expr("price * ? + ?", 2, 100)}) -//// UPDATE "products" SET "code" = 'L1212', "price" = price * '2' + '100', "updated_at" = '2013-11-17 21:34:10' WHERE "id" = '2'; +//// UPDATE "products" SET "price" = price * '2' + '100', "updated_at" = '2013-11-17 21:34:10' WHERE "id" = '2'; DB.Model(&product).UpdateColumn("quantity", gorm.Expr("quantity - ?", 1)) //// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = '2'; @@ -532,6 +557,26 @@ DB.Model(&product).Where("quantity > 1").UpdateColumn("quantity", gorm.Expr("qua //// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = '2' AND quantity > 1; ``` +### Change Updating Values In Callbacks + +If you want to change updating values in callbacks using `BeforeUpdate`, `BeforeSave`, you could use `scope.SetColumn`, for example: + +```go +func (user *User) BeforeSave(scope *gorm.Scope) (err error) { + if pw, err := bcrypt.GenerateFromPassword(user.Password, 0); err == nil { + scope.SetColumn("EncryptedPassword", pw) + } +} +``` + +### Extra Update option + +```go +// Add extra SQL option for updating SQL +db.Model(&user).Set("gorm:update_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Update("name, "hello") +//// UPDATE users SET name='hello', updated_at = '2013-11-17 21:34:10' WHERE id=111 OPTION (OPTIMIZE FOR UNKNOWN); +``` + ## Delete ```go From c89b89677864b155b61aec23832bca5962cded0e Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Fri, 19 Feb 2016 12:04:15 +0800 Subject: [PATCH 55/83] Update README for Creating --- README.md | 88 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 4d078b87..34d0c2d3 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,10 @@ go get -u github.com/jinzhu/gorm - [Migration](#migration) - [Basic CRUD](#basic-crud) - [Create](#create-record) + - [Create With Associations](#create-with-associations) + - [Default Values](#default-values) + - [Setting Primary Key In Callbacks](#setting-primary-key-in-callbacks) + - [Extra Creating option](#extra-creating-option) - [Query](#query) - [Query With Where (Plain SQL)](#query-with-where-plain-sql) - [Query With Where (Struct & Map)](#query-with-where-struct--map) @@ -46,6 +50,7 @@ go get -u github.com/jinzhu/gorm - [Query With Or](#query-with-or) - [Query Chains](#query-chains) - [Preloading (Eager loading)](#preloading-eager-loading) + - [Extra Querying option](#extra-querying-option) - [Update](#update) - [Update All Fields](#update-all-fields) - [Update Changed Fields](#update-changed-fields) @@ -54,7 +59,7 @@ go get -u github.com/jinzhu/gorm - [Batch Updates](#batch-updates) - [Update with SQL Expression](#update-with-sql-expression) - [Change Updating Values In Callbacks](#change-updating-values-in-callbacks) - - [Extra Update option](#extra-update-option) + - [Extra Updating option](#extra-updating-option) - [Delete](#delete) - [Batch Delete](#batch-delete) - [Soft Delete](#soft-delete) @@ -88,7 +93,6 @@ go get -u github.com/jinzhu/gorm - [Existing Schema](#existing-schema) - [Composite Primary Key](#composite-primary-key) - [Database Indexes & Foreign Key](#database-indexes--foreign-key) - - [Default values](#default-values) - [More examples with query chain](#more-examples-with-query-chain) ## Define Models (Structs) @@ -237,7 +241,7 @@ db.AutoMigrate(&User{}, &Product{}, &Order{}) ```go user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()} -db.NewRecord(user) // => returns `true` if primary key is blank +db.NewRecord(user) // => returns `true` as primary key is blank db.Create(&user) @@ -264,15 +268,49 @@ db.Create(&user) //// INSERT INTO "languages" ("name") VALUES ('EN'); //// INSERT INTO user_languages ("user_id","language_id") VALUES (111, 2); //// COMMIT; +``` +### Create With Associations +Refer [Associations](#associations) for more details + +### Default Values + +You could defined default value in the `sql` tag, then the generated creating SQL will ignore these fields that including default value and its value is blank, and after inserted the record into databae, gorm will load those fields's value from database. + +```go +type Animal struct { + ID int64 + Name string `sql:"default:'galeone'"` + Age int64 +} + +var animal = Animal{Age: 99, Name: ""} +db.Create(&animal) +// INSERT INTO animals("age") values('99'); +// SELECT name from animals WHERE ID=111; // the returning primary key is 111 +// animal.Name => 'galeone' +``` + +### Setting Primary Key In Callbacks + +If you want to set primary key in `BeforeCreate` callback, you could use `scope.SetColumn`, for example: + +```go +func (user *User) BeforeCreate(scope *gorm.Scope) error { + scope.SetColumn("ID", uuid.New()) + return nil +} +``` + +### Extra Creating option + +```go // Add extra SQL option for inserting SQL db.Set("gorm:insert_option", "ON CONFLICT").Create(&product) // INSERT INTO products (name, code) VALUES ("name", "code") ON CONFLICT; ``` -Refer [Associations](#associations) for more details - ## Query ```go @@ -291,10 +329,6 @@ db.Find(&users) // Get record with primary key db.First(&user, 10) //// SELECT * FROM users WHERE id = 10; - -// Add extra SQL option for selecting SQL -db.Set("gorm:query_option", "FOR UPDATE").First(&user, 10) -//// SELECT * FROM users WHERE id = 10 FOR UPDATE; ``` ### Query With Where (Plain SQL) @@ -456,6 +490,14 @@ db.Preload("Orders.OrderItems").Find(&users) db.Preload("Orders", "state = ?", "paid").Preload("Orders.OrderItems").Find(&users) ``` +### Extra Querying option + +```go +// Add extra SQL option for selecting SQL +db.Set("gorm:query_option", "FOR UPDATE").First(&user, 10) +//// SELECT * FROM users WHERE id = 10 FOR UPDATE; +``` + ## Update ### Update All Fields @@ -569,7 +611,7 @@ func (user *User) BeforeSave(scope *gorm.Scope) (err error) { } ``` -### Extra Update option +### Extra Updating option ```go // Add extra SQL option for updating SQL @@ -1346,32 +1388,6 @@ db.Model(&User{}).AddUniqueIndex("idx_user_name_age", "name", "age") db.Model(&User{}).RemoveIndex("idx_user_name") ``` -## Default values - -```go -type Animal struct { - ID int64 - Name string `sql:"default:'galeone'"` - Age int64 -} -``` - -If you have defined a default value in the `sql` tag, the generated create SQL will ignore these fields if it is blank. - -Eg. - -```go -db.Create(&Animal{Age: 99, Name: ""}) -``` - -The generated SQL will be: - -```sql -INSERT INTO animals("age") values('99'); -``` - -The same thing occurs in update statements. - ## More examples with query chain ```go From 899996fec9f1894e0b7d5d5cde502a9bb72efdfa Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sat, 20 Feb 2016 22:26:41 +0800 Subject: [PATCH 56/83] Add CHANGELOG --- CHANGELOG.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- utils.go | 7 ++++--- utils_test.go | 1 + 4 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..f653215d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,54 @@ +# Change Log + +## v1.0 (unreleased) + +#### Breaking Changes + +* **`gorm.Open` return type `*gorm.DB` instead of `gorm.DB`** + +* **Updating will only update changed fields** + + Most applications won't be affected, only when you are changing updating values in callbacks like `BeforeSave`, `BeforeUpdate`, you should use `scope.SetColumn` then, for example: + + ```go + func (user *User) BeforeUpdate(scope *gorm.Scope) { + if pw, err := bcrypt.GenerateFromPassword(user.Password, 0); err == nil { + scope.SetColumn("EncryptedPassword", pw) + // user.EncryptedPassword = pw // doesn't work, won't including EncryptedPassword field when updating + } + } + ``` + +* **Soft delete's default querying scope will only check `deleted_at IS NULL`** + + Before `db.Find(&user)` will generate querying SQL if user has `DeletedAt` field + + `SELECT * FROM users WHERE deleted_at IS NULL OR deleted_at <= '0001-01-02'` + + Now won't include blank time check `<= '0001-01-02` anymore, will generat SQL like: + + `SELECT * FROM users WHERE deleted_at IS NULL` + + So your application's `DeletedAt` field should not use `time.Time` as data type, need to use pointer `*time.Time` or something like `NullTime`. + If you are using `gorm.Model`, then you are good, nothing need to be change, just make sure all records using blank time for `deleted_at` has been set to NULL, sample migration script: + + ```go + import ( + "github.com/jinzhu/now" + ) + + func main() { + var models = []interface{}{&User{}, &Image{}} + for _, model := range models { + db.Unscoped().Model(model).Where("deleted_at < ?", now.MustParse("0001-01-02")).Update("deleted_at", gorm.Expr("NULL")) + } + } + ``` + +* **New ToDBName logic** + + Before when GORM convert Struct, Field's name to db name, only some common initialisms from [golint](https://github.com/golang/lint/blob/master/lint.go#L702) like `HTTP`, `URI` are special handled. + + So field `HTTP`'s db name will be `http` not `h_t_t_p`, but other initialisms like `SKU` not in golint, it's db name will be `s_k_u`, this release fixed this, any upper case initialisms should be converted correctly. + + If you applications using some upper case initialisms which doesn't exist in [golint](https://github.com/golang/lint/blob/master/lint.go#L702), you could set the column name with tag, like `sql:"column:s_k_u"`, or alert your database's column name in your database diff --git a/README.md b/README.md index 34d0c2d3..6d467f9a 100644 --- a/README.md +++ b/README.md @@ -653,7 +653,7 @@ db.Where("age = ?", 20).Delete(&User{}) // Soft deleted records will be ignored when query them db.Where("age = 20").Find(&user) -//// SELECT * FROM users WHERE age = 20 AND (deleted_at IS NULL OR deleted_at <= '0001-01-02'); +//// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL; // Find soft deleted records with Unscoped db.Unscoped().Where("age = 20").Find(&users) diff --git a/utils.go b/utils.go index f6bce479..7fc53fa5 100644 --- a/utils.go +++ b/utils.go @@ -76,7 +76,9 @@ func ToDBName(name string) string { if lastCase == upper && nextCase == upper { buf.WriteRune(v) } else { - buf.WriteRune('_') + if value[i-1] != '_' && value[i+1] != '_' { + buf.WriteRune('_') + } buf.WriteRune(v) } } else { @@ -92,8 +94,7 @@ func ToDBName(name string) string { buf.WriteByte(value[len(value)-1]) - s := strings.Replace(strings.ToLower(buf.String()), "__", "_", -1) - + s := strings.ToLower(buf.String()) smap.Set(name, s) return s } diff --git a/utils_test.go b/utils_test.go index 0e88a8b7..07f5b17f 100644 --- a/utils_test.go +++ b/utils_test.go @@ -13,6 +13,7 @@ func TestToDBNameGenerateFriendlyName(t *testing.T) { "PFAndESI": "pf_and_esi", "AbcAndJkl": "abc_and_jkl", "EmployeeID": "employee_id", + "SKU_ID": "sku_id", "HTTPAndSMTP": "http_and_smtp", "HTTPServerHandlerForURLID": "http_server_handler_for_url_id", "UUID": "uuid", From 29f3e6a9279bb4d2d771d0d057e1aaf9b8a4e4a5 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sat, 20 Feb 2016 23:43:51 +0800 Subject: [PATCH 57/83] Update ChangeLog --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f653215d..3c37136e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,8 +47,8 @@ * **New ToDBName logic** - Before when GORM convert Struct, Field's name to db name, only some common initialisms from [golint](https://github.com/golang/lint/blob/master/lint.go#L702) like `HTTP`, `URI` are special handled. + Before when GORM convert Struct, Field's name to db name, only those common initialisms from [golint](https://github.com/golang/lint/blob/master/lint.go#L702) like `HTTP`, `URI` are special handled. - So field `HTTP`'s db name will be `http` not `h_t_t_p`, but other initialisms like `SKU` not in golint, it's db name will be `s_k_u`, this release fixed this, any upper case initialisms should be converted correctly. + So field `HTTP`'s db name will be `http` not `h_t_t_p`, but some other initialisms like `SKU` that not in golint, it's db name will be `s_k_u`, this release fixed this, any upper case initialisms should be converted correctly. - If you applications using some upper case initialisms which doesn't exist in [golint](https://github.com/golang/lint/blob/master/lint.go#L702), you could set the column name with tag, like `sql:"column:s_k_u"`, or alert your database's column name in your database + If your applications using some upper case initialisms which doesn't exist in [golint](https://github.com/golang/lint/blob/master/lint.go#L702), you need to overwrite generated column name with tag, like `sql:"column:s_k_u"`, or alert your database's column name according to new logic From f57198fe97b14fcd6ab9f689b51c7dc73e995386 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 21 Feb 2016 10:52:30 +0800 Subject: [PATCH 58/83] Test multiple Joins --- README.md | 7 ++----- main_test.go | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 6d467f9a..0a1463c0 100644 --- a/README.md +++ b/README.md @@ -1012,11 +1012,8 @@ for rows.Next() { db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results) -// find a user by email address -db.Joins("inner join emails on emails.user_id = users.id").Where("emails.email = ?", "x@example.org").Find(&user) - -// find all email addresses for a user -db.Joins("LEFT JOIN users ON users.id = emails.user_id AND users.name = ?", "jinzhu").Find(&emails) +// multiple joins with parameter +db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?", "411111111111").Find(&user) ``` ## Transactions diff --git a/main_test.go b/main_test.go index 39189cd3..dff91828 100644 --- a/main_test.go +++ b/main_test.go @@ -501,8 +501,9 @@ func TestGroup(t *testing.T) { func TestJoins(t *testing.T) { var user = User{ - Name: "joins", - Emails: []Email{{Email: "join1@example.com"}, {Email: "join2@example.com"}}, + Name: "joins", + CreditCard: CreditCard{Number: "411111111111"}, + Emails: []Email{{Email: "join1@example.com"}, {Email: "join2@example.com"}}, } DB.Save(&user) @@ -517,6 +518,18 @@ func TestJoins(t *testing.T) { if len(users2) != 1 { t.Errorf("should find one users using left join with conditions") } + + var users3 []User + DB.Joins("join emails on emails.user_id = users.id AND emails.email = ?", "join1@example.com").Joins("join credit_cards on credit_cards.user_id = users.id AND credit_cards.number = ?", "411111111111").Where("name = ?", "joins").First(&users3) + if len(users3) != 1 { + t.Errorf("should find one users using multiple left join conditions") + } + + var users4 []User + DB.Joins("join emails on emails.user_id = users.id AND emails.email = ?", "join1@example.com").Joins("join credit_cards on credit_cards.user_id = users.id AND credit_cards.number = ?", "422222222222").Where("name = ?", "joins").First(&users4) + if len(users4) != 0 { + t.Errorf("should find no user when searching with unexisting credit card") + } } func TestJoinsWithSelect(t *testing.T) { From 2a9c03002c0908d4d1ac1aa5ee062ee5ae6af129 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 21 Feb 2016 21:42:26 +0800 Subject: [PATCH 59/83] Update gitbook --- doc/.gitignore | 2 ++ doc/README.md | 3 ++ doc/SUMMARY.md | 32 +++++++++++++++++ doc/book.json | 20 +++++++++++ doc/database/connect-database.md | 33 ++++++++++++++++++ doc/database/database.md | 4 +++ doc/database/migration.md | 59 ++++++++++++++++++++++++++++++++ 7 files changed, 153 insertions(+) create mode 100644 doc/.gitignore create mode 100644 doc/README.md create mode 100644 doc/SUMMARY.md create mode 100644 doc/book.json create mode 100644 doc/database/connect-database.md create mode 100644 doc/database/database.md create mode 100644 doc/database/migration.md diff --git a/doc/.gitignore b/doc/.gitignore new file mode 100644 index 00000000..9f743a65 --- /dev/null +++ b/doc/.gitignore @@ -0,0 +1,2 @@ +/_book +/node_modules diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 00000000..4409686b --- /dev/null +++ b/doc/README.md @@ -0,0 +1,3 @@ +# GORM + +This is GORM diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md new file mode 100644 index 00000000..dea1d177 --- /dev/null +++ b/doc/SUMMARY.md @@ -0,0 +1,32 @@ +# Summary + +* [GORM](README.md) +* [Database](database/database.md) + * [Connecting to a Database](database/connect-database.md) + * [Migration](database/migration.md) + * [Schema]() +* [Models]() + * [Model Defination]() + * [Naming Conventions & Overriding]() + * [Associations]() + * [Belongs To]() + * [Has One]() + * [Has Many]() + * [Many To Many]() + * [Polymorphism]() + * [Association Mode]() +* [CRUD: Reading and Writing Data]() + * [Create]() + * [Query]() + * [Preloading (Eager Loading)]() + * [Update]() + * [Delete / Soft Delete]() + * [Callbacks]() +* [Advanced Usage]() + * [Error Handling]() + * [Transactions]() + * [Raw SQL & SQL Builder]() + * [Composite Primary Key]() + * [Overriding Logger]() +* [Development]() + * [Write Plugins]() diff --git a/doc/book.json b/doc/book.json new file mode 100644 index 00000000..bcf750b9 --- /dev/null +++ b/doc/book.json @@ -0,0 +1,20 @@ +{ + "title": "GORM Guide", + "plugins": [ + "prism", "-highlight", "collapsible-menu", "toc", + "github", "anchors", "edit-link" + ], + "pluginsConfig": { + "toc": { + "addClass": true, + "className": "toc" + }, + "github": { + "url": "https://github.com/jinzhu/gorm" + }, + "edit-link": { + "base": "https://github.com/jinzhu/gorm/edit/gh-pages", + "label": "Edit This Page" + } + } +} diff --git a/doc/database/connect-database.md b/doc/database/connect-database.md new file mode 100644 index 00000000..29432a16 --- /dev/null +++ b/doc/database/connect-database.md @@ -0,0 +1,33 @@ +### Connecting To A Database + +```go +import ( + "github.com/jinzhu/gorm" + _ "github.com/lib/pq" + _ "github.com/go-sql-driver/mysql" + _ "github.com/mattn/go-sqlite3" +) + +func init() { + db, err := gorm.Open("postgres", "user=gorm dbname=gorm sslmode=disable") + // db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local") + // db, err := gorm.Open("sqlite3", "/tmp/gorm.db") + + // Use existing database connection + dbSql, err := sql.Open("postgres", "user=gorm dbname=gorm sslmode=disable") + db, err := gorm.Open("postgres", dbSql) +} +``` + +```go +// Get database connection handle [*sql.DB](http://golang.org/pkg/database/sql/#DB) +db.DB() + +// Then you could invoke `*sql.DB`'s functions with it +db.DB().Ping() +db.DB().SetMaxIdleConns(10) +db.DB().SetMaxOpenConns(100) + +// Disable table name's pluralization +db.SingularTable(true) +``` diff --git a/doc/database/database.md b/doc/database/database.md new file mode 100644 index 00000000..ccd8d176 --- /dev/null +++ b/doc/database/database.md @@ -0,0 +1,4 @@ +## Database + + + diff --git a/doc/database/migration.md b/doc/database/migration.md new file mode 100644 index 00000000..6ff8f080 --- /dev/null +++ b/doc/database/migration.md @@ -0,0 +1,59 @@ +## Migration + + + +### Auto Migration + +Automatically migrate your schema, to keep your schema update to date + +**WARNING** AutoMigrate will ONLY create tables, columns and indexes if doesn't exist, +WON'T change existing column's type or delete unused columns to protect your data + +```go +db.AutoMigrate(&User{}) + +db.AutoMigrate(&User{}, &Product{}, &Order{}) + +// Add table suffix when create tables +db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{}) +``` + +### Has Table + +```go +// Check if model `User`'s table has been created or not +db.HasTable(&User{}) + +// Check table `users` exists or not +db.HasTable("users") +``` + +### Create Table + +```go +db.CreateTable(&User{}) + +db.Set("gorm:table_options", "ENGINE=InnoDB").CreateTable(&User{}) +// will append "ENGINE=InnoDB" to the SQL statement when creating table `users` +``` + +### Drop table + +```go +db.DropTable(&User{}) +``` + +### ModifyColumn + +Change column's type + +```go +// change column description's data type to `text` for model `User`'s table +db.Model(&User{}).ModifyColumn("description", "text") +``` + +### DropColumn + +```go +db.Model(&User{}).DropColumn("description") +``` From 118c8b836f7fb3e81fc4f89268f3c72d011576f5 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 21 Feb 2016 22:40:01 +0800 Subject: [PATCH 60/83] Update README --- doc/SUMMARY.md | 10 +++++++++- doc/book.json | 17 +++++++++++++++++ doc/database/database.md | 20 +++++++++++++++++++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md index dea1d177..28c2ffdb 100644 --- a/doc/SUMMARY.md +++ b/doc/SUMMARY.md @@ -1,6 +1,14 @@ # Summary -* [GORM](README.md) +* [GORM GITHUB](http://github.com/jinzhu/gorm) +* [Getting Start](README.md) +{% for path, chapter in book.chapters %} +* [{{ chapter.name }}]({{ path }}) + {% for section in chapter.sections %} + * [{{ section.name }}]({{ section.path }}) + {% endfor %} +{% endfor %} + * [Database](database/database.md) * [Connecting to a Database](database/connect-database.md) * [Migration](database/migration.md) diff --git a/doc/book.json b/doc/book.json index bcf750b9..65889b54 100644 --- a/doc/book.json +++ b/doc/book.json @@ -16,5 +16,22 @@ "base": "https://github.com/jinzhu/gorm/edit/gh-pages", "label": "Edit This Page" } + }, + "sections": [ + { + "content": "

Hello

", + "type": "normal" + } + ], + "variables": { + "chapters": { + "database/database.md": { + "name": "Databae", + "sections": [ + {"name": "Connection to a Database", "path": "database/connect-database.md"}, + {"name": "Migration", "path": "database/migration.md"} + ] + } + } } } diff --git a/doc/database/database.md b/doc/database/database.md index ccd8d176..913a283f 100644 --- a/doc/database/database.md +++ b/doc/database/database.md @@ -1,4 +1,22 @@ ## Database - +{% for section in book.chapters["database/database.md"].sections %} +* [**{{section.name}}**](../{{section.path}}) +{% endfor %} +{{book.chapters}} + From 9a79822ff2156d96657288e64eba23c93015ecf0 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 21 Feb 2016 23:09:24 +0800 Subject: [PATCH 61/83] Update Document --- doc/SUMMARY.md | 36 ++---------------- doc/associations/associations.md | 0 doc/associations/belongs-to.md | 0 doc/book.json | 50 +++++++++++++++++++++++- doc/database/connect-database.md | 65 ++++++++++++++++++++++++-------- doc/database/database.md | 19 +--------- doc/database/migration.md | 30 +++++++++++++++ doc/models/defination.md | 0 doc/models/models.md | 8 ++++ doc/models/naming-conventions.md | 0 10 files changed, 140 insertions(+), 68 deletions(-) create mode 100644 doc/associations/associations.md create mode 100644 doc/associations/belongs-to.md create mode 100644 doc/models/defination.md create mode 100644 doc/models/models.md create mode 100644 doc/models/naming-conventions.md diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md index 28c2ffdb..7195a268 100644 --- a/doc/SUMMARY.md +++ b/doc/SUMMARY.md @@ -1,40 +1,12 @@ # Summary * [GORM GITHUB](http://github.com/jinzhu/gorm) -* [Getting Start](README.md) +* [Getting Started with GORM](README.md) {% for path, chapter in book.chapters %} * [{{ chapter.name }}]({{ path }}) {% for section in chapter.sections %} * [{{ section.name }}]({{ section.path }}) - {% endfor %} + {% if section["sections"] %}{% for subsection in section.sections %} + * [{{ subsection.name }}]({{ subsection.path }}) + {% endfor %}{% endif %}{% endfor %} {% endfor %} - -* [Database](database/database.md) - * [Connecting to a Database](database/connect-database.md) - * [Migration](database/migration.md) - * [Schema]() -* [Models]() - * [Model Defination]() - * [Naming Conventions & Overriding]() - * [Associations]() - * [Belongs To]() - * [Has One]() - * [Has Many]() - * [Many To Many]() - * [Polymorphism]() - * [Association Mode]() -* [CRUD: Reading and Writing Data]() - * [Create]() - * [Query]() - * [Preloading (Eager Loading)]() - * [Update]() - * [Delete / Soft Delete]() - * [Callbacks]() -* [Advanced Usage]() - * [Error Handling]() - * [Transactions]() - * [Raw SQL & SQL Builder]() - * [Composite Primary Key]() - * [Overriding Logger]() -* [Development]() - * [Write Plugins]() diff --git a/doc/associations/associations.md b/doc/associations/associations.md new file mode 100644 index 00000000..e69de29b diff --git a/doc/associations/belongs-to.md b/doc/associations/belongs-to.md new file mode 100644 index 00000000..e69de29b diff --git a/doc/book.json b/doc/book.json index 65889b54..9e347864 100644 --- a/doc/book.json +++ b/doc/book.json @@ -13,7 +13,7 @@ "url": "https://github.com/jinzhu/gorm" }, "edit-link": { - "base": "https://github.com/jinzhu/gorm/edit/gh-pages", + "base": "https://github.com/jinzhu/gorm/edit/master", "label": "Edit This Page" } }, @@ -28,9 +28,55 @@ "database/database.md": { "name": "Databae", "sections": [ - {"name": "Connection to a Database", "path": "database/connect-database.md"}, + {"name": "Database Connection", "path": "database/connect-database.md"}, {"name": "Migration", "path": "database/migration.md"} ] + }, + "models/models.md": { + "name": "Models", + "sections": [ + {"name": "Model Defination", "path": "models/defination.md"}, + {"name": "Naming Conventions & Overriding", "path": "models/naming-conventions.md"}, + {"name": "Associations", "path": "associations/associations.md", "sections": + [ + {"name": "Belongs To", "path": "associations/belongs-to.md"}, + {"name": "Has One", "path": "associations/has-one.md"}, + {"name": "Has Many", "path": "associations/has-many.md"}, + {"name": "Many To Many", "path": "associations/many-to-many.md"}, + {"name": "Polymorphism", "path": "associations/polymorphism.md"}, + {"name": "Association Mode", "path": "associations/association-mode.md"} + ] + } + ] + }, + "curd/curd.md": { + "name": "CRUD: Reading and Writing Data", + "sections": [ + {"name": "Create", "path": "curd/create.md"}, + {"name": "Query", "path": "curd/query.md"}, + {"name": "Preloading (Eager Loading)", "path": "curd/preloading.md"}, + {"name": "Update", "path": "curd/update.md"}, + {"name": "Delete / Soft Delete", "path": "curd/delete.md"} + ] + }, + "callbacks/callbacks.md": { + "name": "Callbacks" + }, + "advanced/advanced.md": { + "name": "Advanced Usage", + "sections": [ + {"name": "Error Handling", "path": "advanced/error-handling.md"}, + {"name": "Transactions", "path": "advanced/transactions.md"}, + {"name": "Raw SQL & SQL Builder", "path": "advanced/sql-builder.md"}, + {"name": "Composite Primary Key", "path": "advanced/compose-primary-key.md"}, + {"name": "Overriding Logger", "path": "advanced/logger.md"} + ] + }, + "development/development.md": { + "name": "Development", + "sections": [ + {"name": "Write Plugins", "path": "development/plugins.md"} + ] } } } diff --git a/doc/database/connect-database.md b/doc/database/connect-database.md index 29432a16..66f04509 100644 --- a/doc/database/connect-database.md +++ b/doc/database/connect-database.md @@ -1,33 +1,66 @@ -### Connecting To A Database +# Database Connection + + + +## Connecting to a database + +#### MySQL + +**NOTE** don't forgot params `parseTime` to handle data type `time.Time`, [more support parameters](https://github.com/go-sql-driver/mysql#parameters) + +```go +import ( + "github.com/jinzhu/gorm" + _ "github.com/go-sql-driver/mysql" +) +func main() { + db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local") +} +``` + +#### PostgreSQL ```go import ( "github.com/jinzhu/gorm" _ "github.com/lib/pq" - _ "github.com/go-sql-driver/mysql" - _ "github.com/mattn/go-sqlite3" ) - -func init() { +func main() { db, err := gorm.Open("postgres", "user=gorm dbname=gorm sslmode=disable") - // db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local") - // db, err := gorm.Open("sqlite3", "/tmp/gorm.db") - - // Use existing database connection - dbSql, err := sql.Open("postgres", "user=gorm dbname=gorm sslmode=disable") - db, err := gorm.Open("postgres", dbSql) } ``` +#### Sqlite3 + ```go -// Get database connection handle [*sql.DB](http://golang.org/pkg/database/sql/#DB) +import ( + "github.com/jinzhu/gorm" + _ "github.com/mattn/go-sqlite3" +) +func main() { + db, err := gorm.Open("sqlite3", "/tmp/gorm.db") +} +``` + +#### Write Dialect for unsupported databases + +GORM officially support above databases, for unsupported databaes, you could write a dialect for that. + +Refer: https://github.com/jinzhu/gorm/blob/master/dialect.go + + +## Generic database object *sql.DB + +[*sql.DB](http://golang.org/pkg/database/sql/#DB) + +```go +// Get generic database object *sql.DB to use its functions db.DB() -// Then you could invoke `*sql.DB`'s functions with it -db.DB().Ping() +// Connection Pool db.DB().SetMaxIdleConns(10) db.DB().SetMaxOpenConns(100) -// Disable table name's pluralization -db.SingularTable(true) + // Ping +db.DB().Ping() ``` diff --git a/doc/database/database.md b/doc/database/database.md index 913a283f..0733f61b 100644 --- a/doc/database/database.md +++ b/doc/database/database.md @@ -1,22 +1,5 @@ -## Database +# Database {% for section in book.chapters["database/database.md"].sections %} * [**{{section.name}}**](../{{section.path}}) {% endfor %} -{{book.chapters}} - - diff --git a/doc/database/migration.md b/doc/database/migration.md index 6ff8f080..0ca27a13 100644 --- a/doc/database/migration.md +++ b/doc/database/migration.md @@ -57,3 +57,33 @@ db.Model(&User{}).ModifyColumn("description", "text") ```go db.Model(&User{}).DropColumn("description") ``` + +### Add Foreign Key + +```go +// Add foreign key +// 1st param : foreignkey field +// 2nd param : destination table(id) +// 3rd param : ONDELETE +// 4th param : ONUPDATE +db.Model(&User{}).AddForeignKey("city_id", "cities(id)", "RESTRICT", "RESTRICT") +``` + +### Indexes + +```go +// Add index +db.Model(&User{}).AddIndex("idx_user_name", "name") + +// Multiple column index +db.Model(&User{}).AddIndex("idx_user_name_age", "name", "age") + +// Add unique index +db.Model(&User{}).AddUniqueIndex("idx_user_name", "name") + +// Multiple column unique index +db.Model(&User{}).AddUniqueIndex("idx_user_name_age", "name", "age") + +// Remove index +db.Model(&User{}).RemoveIndex("idx_user_name") +``` diff --git a/doc/models/defination.md b/doc/models/defination.md new file mode 100644 index 00000000..e69de29b diff --git a/doc/models/models.md b/doc/models/models.md new file mode 100644 index 00000000..46abf2da --- /dev/null +++ b/doc/models/models.md @@ -0,0 +1,8 @@ +# Models + +{% for section in book.chapters["models/models.md"].sections %} +* [**{{section.name}}**](../{{section.path}}) +{% if section["sections"] %}{% for subsection in section.sections %} + * [**{{ subsection.name }}**]({{ subsection.path }}) +{% endfor %}{% endif %} +{% endfor %} diff --git a/doc/models/naming-conventions.md b/doc/models/naming-conventions.md new file mode 100644 index 00000000..e69de29b From 15fd62a0d15662ba0fb334a5bcce29dedf663da3 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 22 Feb 2016 09:17:05 +0800 Subject: [PATCH 62/83] Update document --- doc/SUMMARY.md | 3 +- doc/models/defination.md | 81 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md index 7195a268..fee9b9e7 100644 --- a/doc/SUMMARY.md +++ b/doc/SUMMARY.md @@ -1,6 +1,7 @@ # Summary -* [GORM GITHUB](http://github.com/jinzhu/gorm) +* [GORM Guides](http://github.com/jinzhu/gorm) + * [Getting Started with GORM](README.md) {% for path, chapter in book.chapters %} * [{{ chapter.name }}]({{ path }}) diff --git a/doc/models/defination.md b/doc/models/defination.md index e69de29b..82106bde 100644 --- a/doc/models/defination.md +++ b/doc/models/defination.md @@ -0,0 +1,81 @@ +# Models + + + +## Model Defination + +```go +type User struct { + ID int + Birthday time.Time + Age int + Name string `sql:"size:255"` // Default size for string is 255, you could reset it with this tag + Num int `sql:"AUTO_INCREMENT"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt *time.Time + IgnoreMe int `sql:"-"` // Ignore this field +} +``` + +## Conventions & Overriding Conventions + +### Table name is the pluralized version of struct name + +```go +type User struct {} // default table name is `users` + +// set User's table name to be `profiles +type (User) TableName() string { + return "profiles" +} + +// Or disable table name's pluralization globally +db.SingularTable(true) // if set this to true, then default table name will be `user`, table name setted with `TableName` won't be affected +``` + +### Column name is the snake case of field's name + +```go +type User struct { + ID uint // column name will be `id` + Name string // column name will be `name` + Birthday time.Time // column name will be `birthday` + CreatedAt time.Time // column name will be `created_at` +} + +type Animal struct { + AnimalId int64 `gorm:"column:beast_id"` // set column name to be `beast_id` + Birthday time.Time `gorm:"column:day_of_the_beast"` // set column name to be `day_of_the_beast` + Age int64 `gorm:"column:age_of_the_beast"` // set column name to be `age_of_the_beast` +} +``` + +### Default use field `ID` as primary key + +```go +type User struct { + ID uint // field `ID` is the default primary key for `User` + Name string +} + +type Animal struct { + // tag `primary_key` used to set `AnimalId` to be primary key + AnimalId int64 `gorm:"primary_key"` + Name string + Age int64 +} +``` + +### Use `CreatedAt` to store record's created time if field exists + +```go +db.Create(&user) // will set field `CreatedAt`'s time to time now + +// If you want to change its value, use `Update` +db.Model(&user).Update("CreatedAt", time.Now()) +``` + +### Use `UpdatedAt` to store record's updated time if field exists +### Use `DeletedAt` to store record's deleted time if field exists +### Gorm provide a default model struct, you could embed it in your struct From ba8b2857ea614fc73bda0d22afa27ace8ab3ba56 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Wed, 24 Feb 2016 22:03:43 +0800 Subject: [PATCH 63/83] Update document --- doc/.gitignore | 1 + doc/README.md | 46 ++- doc/advanced/compose-primary-key.md | 8 + doc/advanced/error-handling.md | 22 ++ doc/advanced/logger.md | 24 ++ doc/advanced/query-chain.md | 38 +++ doc/advanced/sql-builder.md | 45 +++ doc/advanced/transaction.md | 41 +++ doc/associations/association-mode.md | 40 +++ doc/associations/associations.md | 8 + doc/associations/belongs-to.md | 18 ++ doc/associations/has-many.md | 18 ++ doc/associations/has-one.md | 22 ++ doc/associations/many-to-many.md | 17 ++ doc/associations/polymorphism.md | 26 ++ doc/callbacks/callbacks.md | 76 +++++ doc/curd/create.md | 75 +++++ doc/curd/curd.md | 9 + doc/curd/delete.md | 44 +++ doc/curd/preloading.md | 40 +++ doc/curd/query.md | 426 +++++++++++++++++++++++++++ doc/curd/update.md | 120 ++++++++ doc/{ => development}/development.md | 0 doc/models/defination.md | 66 +++-- 24 files changed, 1210 insertions(+), 20 deletions(-) create mode 100644 doc/advanced/compose-primary-key.md create mode 100644 doc/advanced/error-handling.md create mode 100644 doc/advanced/logger.md create mode 100644 doc/advanced/query-chain.md create mode 100644 doc/advanced/sql-builder.md create mode 100644 doc/advanced/transaction.md create mode 100644 doc/associations/association-mode.md create mode 100644 doc/associations/has-many.md create mode 100644 doc/associations/has-one.md create mode 100644 doc/associations/many-to-many.md create mode 100644 doc/associations/polymorphism.md create mode 100644 doc/callbacks/callbacks.md create mode 100644 doc/curd/create.md create mode 100644 doc/curd/curd.md create mode 100644 doc/curd/delete.md create mode 100644 doc/curd/preloading.md create mode 100644 doc/curd/query.md create mode 100644 doc/curd/update.md rename doc/{ => development}/development.md (100%) diff --git a/doc/.gitignore b/doc/.gitignore index 9f743a65..a6fc88a6 100644 --- a/doc/.gitignore +++ b/doc/.gitignore @@ -1,2 +1,3 @@ /_book /node_modules +/gitbook diff --git a/doc/README.md b/doc/README.md index 4409686b..a95b2959 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,3 +1,45 @@ -# GORM +# GORM Guides -This is GORM +The fantastic ORM library for Golang, aims to be developer friendly. + +[![wercker status](https://app.wercker.com/status/0cb7bb1039e21b74f8274941428e0921/s/master "wercker status")](https://app.wercker.com/project/bykey/0cb7bb1039e21b74f8274941428e0921) +[![GoDoc](https://godoc.org/github.com/jinzhu/gorm?status.svg)](https://godoc.org/github.com/jinzhu/gorm) +[![Join the chat at https://gitter.im/jinzhu/gorm](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jinzhu/gorm?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +## Overview + +* Full-Featured ORM (almost) +* Chainable API +* Auto Migrations +* Relations (Has One, Has Many, Belongs To, Many To Many, [Polymorphism](#polymorphism)) +* Callbacks (Before/After Create/Save/Update/Delete/Find) +* Preloading (eager loading) +* Transactions +* Embed Anonymous Struct +* Soft Deletes +* Customizable Logger +* Iteration Support via [Rows](#row--rows) +* Every feature comes with tests +* Developer Friendly + +## Install + +``` +go get -u github.com/jinzhu/gorm +``` + +# Author + +**jinzhu** + +* +* +* + +# Contributors + +https://github.com/jinzhu/gorm/graphs/contributors + +## License + +Released under the [MIT License](https://github.com/jinzhu/gorm/blob/master/License). diff --git a/doc/advanced/compose-primary-key.md b/doc/advanced/compose-primary-key.md new file mode 100644 index 00000000..b8bae65e --- /dev/null +++ b/doc/advanced/compose-primary-key.md @@ -0,0 +1,8 @@ +# Composite Primary Key + +```go +type Product struct { + ID string `gorm:"primary_key"` + LanguageCode string `gorm:"primary_key"` +} +``` diff --git a/doc/advanced/error-handling.md b/doc/advanced/error-handling.md new file mode 100644 index 00000000..7215498f --- /dev/null +++ b/doc/advanced/error-handling.md @@ -0,0 +1,22 @@ +# Error Handling + +```go +query := db.Where("name = ?", "jinzhu").First(&user) +query := db.First(&user).Limit(10).Find(&users) +// query.Error will return the last happened error + +// So you could do error handing in your application like this: +if err := db.Where("name = ?", "jinzhu").First(&user).Error; err != nil { + // error handling... +} + +// RecordNotFound +// If no record found when you query data, gorm will return RecordNotFound error, you could check it like this: +db.Where("name = ?", "hello world").First(&User{}).Error == gorm.RecordNotFound +// Or use the shortcut method +db.Where("name = ?", "hello world").First(&user).RecordNotFound() + +if db.Model(&user).Related(&credit_card).RecordNotFound() { + // no credit card found error handling +} +``` diff --git a/doc/advanced/logger.md b/doc/advanced/logger.md new file mode 100644 index 00000000..b45300d1 --- /dev/null +++ b/doc/advanced/logger.md @@ -0,0 +1,24 @@ +# Logger + +Gorm has built-in logger support + +```go +// Enable Logger +db.LogMode(true) + +// Diable Logger +db.LogMode(false) + +// Debug a single operation +db.Debug().Where("name = ?", "jinzhu").First(&User{}) +``` + +![logger](https://raw.github.com/jinzhu/gorm/master/doc/logger.png) + +### Customize Logger + +```go +// Refer gorm's default logger for how to: https://github.com/jinzhu/gorm/blob/master/logger.go#files +db.SetLogger(gorm.Logger{revel.TRACE}) +db.SetLogger(log.New(os.Stdout, "\r\n", 0)) +``` diff --git a/doc/advanced/query-chain.md b/doc/advanced/query-chain.md new file mode 100644 index 00000000..c3691a91 --- /dev/null +++ b/doc/advanced/query-chain.md @@ -0,0 +1,38 @@ +# Query Chain + +```go +db.First(&first_article).Count(&total_count).Limit(10).Find(&first_page_articles).Offset(10).Find(&second_page_articles) +//// SELECT * FROM articles LIMIT 1; (first_article) +//// SELECT count(*) FROM articles; (total_count) +//// SELECT * FROM articles LIMIT 10; (first_page_articles) +//// SELECT * FROM articles LIMIT 10 OFFSET 10; (second_page_articles) + + +db.Where("created_at > ?", "2013-10-10").Find(&cancelled_orders, "state = ?", "cancelled").Find(&shipped_orders, "state = ?", "shipped") +//// SELECT * FROM orders WHERE created_at > '2013/10/10' AND state = 'cancelled'; (cancelled_orders) +//// SELECT * FROM orders WHERE created_at > '2013/10/10' AND state = 'shipped'; (shipped_orders) + + +// Use variables to keep query chain +todays_orders := db.Where("created_at > ?", "2013-10-29") +cancelled_orders := todays_orders.Where("state = ?", "cancelled") +shipped_orders := todays_orders.Where("state = ?", "shipped") + + +// Search with shared conditions for different tables +db.Where("product_name = ?", "fancy_product").Find(&orders).Find(&shopping_carts) +//// SELECT * FROM orders WHERE product_name = 'fancy_product'; (orders) +//// SELECT * FROM carts WHERE product_name = 'fancy_product'; (shopping_carts) + + +// Search with shared conditions from different tables with specified table +db.Where("mail_type = ?", "TEXT").Find(&users1).Table("deleted_users").Find(&users2) +//// SELECT * FROM users WHERE mail_type = 'TEXT'; (users1) +//// SELECT * FROM deleted_users WHERE mail_type = 'TEXT'; (users2) + + +// FirstOrCreate example +db.Where("email = ?", "x@example.org").Attrs(User{RegisteredIp: "111.111.111.111"}).FirstOrCreate(&user) +//// SELECT * FROM users WHERE email = 'x@example.org'; +//// INSERT INTO "users" (email,registered_ip) VALUES ("x@example.org", "111.111.111.111") // if record not found +``` diff --git a/doc/advanced/sql-builder.md b/doc/advanced/sql-builder.md new file mode 100644 index 00000000..832b6cfc --- /dev/null +++ b/doc/advanced/sql-builder.md @@ -0,0 +1,45 @@ +## Raw SQL + +```go +db.Exec("DROP TABLE users;") +db.Exec("UPDATE orders SET shipped_at=? WHERE id IN (?)", time.Now, []int64{11,22,33}) +``` + +## Row & Rows + +It is even possible to get query result as `*sql.Row` or `*sql.Rows` + +```go +row := db.Table("users").Where("name = ?", "jinzhu").Select("name, age").Row() // (*sql.Row) +row.Scan(&name, &age) + +rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error) +defer rows.Close() +for rows.Next() { + ... + rows.Scan(&name, &age, &email) + ... +} + +// Raw SQL +rows, err := db.Raw("select name, age, email from users where name = ?", "jinzhu").Rows() // (*sql.Rows, error) +defer rows.Close() +for rows.Next() { + ... + rows.Scan(&name, &age, &email) + ... +} +``` + +### Scan Rows + +```go +rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error) +defer rows.Close() + +for rows.Next() { + var user User + db.ScanRows(rows, &user) + // do something +} +``` diff --git a/doc/advanced/transaction.md b/doc/advanced/transaction.md new file mode 100644 index 00000000..bef82561 --- /dev/null +++ b/doc/advanced/transaction.md @@ -0,0 +1,41 @@ +## Transactions + +To perform a set of operations within a transaction, the general flow is as below. +The database handle returned from ``` db.Begin() ``` should be used for all operations within the transaction. +(Note that all individual save and delete operations are run in a transaction by default.) + +```go +// begin +tx := db.Begin() + +// do some database operations (use 'tx' from this point, not 'db') +tx.Create(...) +... + +// rollback in case of error +tx.Rollback() + +// Or commit if all is ok +tx.Commit() +``` + +### A Specific Example +``` +func CreateAnimals(db *gorm.DB) err { + tx := db.Begin() + // Note the use of tx as the database handle once you are within a transaction + + if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil { + tx.Rollback() + return err + } + + if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil { + tx.Rollback() + return err + } + + tx.Commit() + return nil +} +``` diff --git a/doc/associations/association-mode.md b/doc/associations/association-mode.md new file mode 100644 index 00000000..ca15c962 --- /dev/null +++ b/doc/associations/association-mode.md @@ -0,0 +1,40 @@ +# Association Mode + +Association Mode contains some helper methods to handle relationship things easily. + +```go +// Start Association Mode +var user User +db.Model(&user).Association("Languages") +// `user` is the source, it need to be a valid record (contains primary key) +// `Languages` is source's field name for a relationship. +// If those conditions not matched, will return an error, check it with: +// db.Model(&user).Association("Languages").Error + + +// Query - Find out all related associations +db.Model(&user).Association("Languages").Find(&languages) + + +// Append - Append new associations for many2many, has_many, will replace current association for has_one, belongs_to +db.Model(&user).Association("Languages").Append([]Language{languageZH, languageEN}) +db.Model(&user).Association("Languages").Append(Language{Name: "DE"}) + + +// Delete - Remove relationship between source & passed arguments, won't delete those arguments +db.Model(&user).Association("Languages").Delete([]Language{languageZH, languageEN}) +db.Model(&user).Association("Languages").Delete(languageZH, languageEN) + + +// Replace - Replace current associations with new one +db.Model(&user).Association("Languages").Replace([]Language{languageZH, languageEN}) +db.Model(&user).Association("Languages").Replace(Language{Name: "DE"}, languageEN) + + +// Count - Return the count of current associations +db.Model(&user).Association("Languages").Count() + + +// Clear - Remove relationship between source & current associations, won't delete those associations +db.Model(&user).Association("Languages").Clear() +``` diff --git a/doc/associations/associations.md b/doc/associations/associations.md index e69de29b..b0fc3ec4 100644 --- a/doc/associations/associations.md +++ b/doc/associations/associations.md @@ -0,0 +1,8 @@ +# Associations + +{% for section in book.chapters["associations/associations.md"].sections %} +* [**{{section.name}}**](../{{section.path}}) +{% if section["sections"] %}{% for subsection in section.sections %} + * [**{{ subsection.name }}**]({{ subsection.path }}) +{% endfor %}{% endif %} +{% endfor %} diff --git a/doc/associations/belongs-to.md b/doc/associations/belongs-to.md index e69de29b..d7ae8667 100644 --- a/doc/associations/belongs-to.md +++ b/doc/associations/belongs-to.md @@ -0,0 +1,18 @@ +# Belongs To + +```go +// User belongs to a profile, ProfileID is the foreign key +type User struct { + gorm.Model + Profile Profile + ProfileID int +} + +type Profile struct { + gorm.Model + Name string +} + +db.Model(&user).Related(&profile) +//// SELECT * FROM profiles WHERE id = 111; // 111 is user's foreign key ProfileID +``` diff --git a/doc/associations/has-many.md b/doc/associations/has-many.md new file mode 100644 index 00000000..5cf15a47 --- /dev/null +++ b/doc/associations/has-many.md @@ -0,0 +1,18 @@ +# Has Many + +```go +// User has many emails, UserID is the foreign key +type User struct { + gorm.Model + Emails []Email +} + +type Email struct { + gorm.Model + Email string + UserID uint +} + +db.Model(&user).Related(&emails) +//// SELECT * FROM emails WHERE user_id = 111; // 111 is user's primary key +``` diff --git a/doc/associations/has-one.md b/doc/associations/has-one.md new file mode 100644 index 00000000..31bbacad --- /dev/null +++ b/doc/associations/has-one.md @@ -0,0 +1,22 @@ +# Has One + +```go +// User has one CreditCard, UserID is the foreign key +type User struct { + gorm.Model + CreditCard CreditCard +} + +type CreditCard struct { + gorm.Model + UserID uint + Number string +} + +var card CreditCard +db.Model(&user).Related(&card, "CreditCard") +//// SELECT * FROM credit_cards WHERE user_id = 123; // 123 is user's primary key +// CreditCard is user's field name, it means get user's CreditCard relations and fill it into variable card +// If the field name is same as the variable's type name, like above example, it could be omitted, like: +db.Model(&user).Related(&card) +``` diff --git a/doc/associations/many-to-many.md b/doc/associations/many-to-many.md new file mode 100644 index 00000000..af11dd35 --- /dev/null +++ b/doc/associations/many-to-many.md @@ -0,0 +1,17 @@ +# Many To Many + +```go +// User has and belongs to many languages, use `user_languages` as join table +type User struct { + gorm.Model + Languages []Language `gorm:"many2many:user_languages;"` +} + +type Language struct { + gorm.Model + Name string +} + +db.Model(&user).Related(&languages) +//// SELECT * FROM "languages" INNER JOIN "user_languages" ON "user_languages"."language_id" = "languages"."id" WHERE "user_languages"."user_id" = 111 +``` diff --git a/doc/associations/polymorphism.md b/doc/associations/polymorphism.md new file mode 100644 index 00000000..10e2307d --- /dev/null +++ b/doc/associations/polymorphism.md @@ -0,0 +1,26 @@ +# Polymorphism + +Supports polymorphic has-many and has-one associations. + +```go + type Cat struct { + Id int + Name string + Toy Toy `gorm:"polymorphic:Owner;"` + } + + type Dog struct { + Id int + Name string + Toy Toy `gorm:"polymorphic:Owner;"` + } + + type Toy struct { + Id int + Name string + OwnerId int + OwnerType string + } +``` + +Note: polymorphic belongs-to and many-to-many are explicitly NOT supported, and will throw errors. diff --git a/doc/callbacks/callbacks.md b/doc/callbacks/callbacks.md new file mode 100644 index 00000000..56c29193 --- /dev/null +++ b/doc/callbacks/callbacks.md @@ -0,0 +1,76 @@ +# Callbacks + +Callbacks are methods defined on the pointer of struct. +If any callback returns an error, gorm will stop future operations and rollback all changes. + +Here is the list of all available callbacks: +(listed in the same order in which they will get called during the respective operations) + +### Creating An Object + +```go +BeforeSave +BeforeCreate +// save before associations +// save self +// save after associations +AfterCreate +AfterSave +``` +### Updating An Object + +```go +BeforeSave +BeforeUpdate +// save before associations +// save self +// save after associations +AfterUpdate +AfterSave +``` + +### Destroying An Object + +```go +BeforeDelete +// delete self +AfterDelete +``` + +### After Find + +```go +// load data from database +AfterFind +``` + +### Example + +```go +func (u *User) BeforeUpdate() (err error) { + if u.readonly() { + err = errors.New("read only user") + } + return +} + +// Rollback the insertion if user's id greater than 1000 +func (u *User) AfterCreate() (err error) { + if (u.Id > 1000) { + err = errors.New("user id is already greater than 1000") + } + return +} +``` + +Save/delete operations in gorm are running in a transaction. +Changes made in that transaction are not visible unless it is commited. +So if you want to use those changes in your callbacks, you need to run your SQL in the same transaction. +For this Gorm supports passing transactions to callbacks like this: + +```go +func (u *User) AfterCreate(tx *gorm.DB) (err error) { + tx.Model(u).Update("role", "admin") + return +} +``` diff --git a/doc/curd/create.md b/doc/curd/create.md new file mode 100644 index 00000000..c450d902 --- /dev/null +++ b/doc/curd/create.md @@ -0,0 +1,75 @@ +# Create + +### Create Record + +```go +user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()} + +db.NewRecord(user) // => returns `true` as primary key is blank + +db.Create(&user) + +db.NewRecord(user) // => return `false` after `user` created + +// Associations will be inserted automatically when save the record +user := User{ + Name: "jinzhu", + BillingAddress: Address{Address1: "Billing Address - Address 1"}, + ShippingAddress: Address{Address1: "Shipping Address - Address 1"}, + Emails: []Email{{Email: "jinzhu@example.com"}, {Email: "jinzhu-2@example@example.com"}}, + Languages: []Language{{Name: "ZH"}, {Name: "EN"}}, +} + +db.Create(&user) +//// BEGIN TRANSACTION; +//// INSERT INTO "addresses" (address1) VALUES ("Billing Address - Address 1"); +//// INSERT INTO "addresses" (address1) VALUES ("Shipping Address - Address 1"); +//// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2); +//// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com"); +//// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu-2@example.com"); +//// INSERT INTO "languages" ("name") VALUES ('ZH'); +//// INSERT INTO user_languages ("user_id","language_id") VALUES (111, 1); +//// INSERT INTO "languages" ("name") VALUES ('EN'); +//// INSERT INTO user_languages ("user_id","language_id") VALUES (111, 2); +//// COMMIT; +``` + +### Create With Associations + +Refer Associations for more details + +### Default Values + +You could defined default value in the `sql` tag, then the generated creating SQL will ignore these fields that including default value and its value is blank, and after inserted the record into databae, gorm will load those fields's value from database. + +```go +type Animal struct { + ID int64 + Name string `sql:"default:'galeone'"` + Age int64 +} + +var animal = Animal{Age: 99, Name: ""} +db.Create(&animal) +// INSERT INTO animals("age") values('99'); +// SELECT name from animals WHERE ID=111; // the returning primary key is 111 +// animal.Name => 'galeone' +``` + +### Setting Primary Key In Callbacks + +If you want to set primary key in `BeforeCreate` callback, you could use `scope.SetColumn`, for example: + +```go +func (user *User) BeforeCreate(scope *gorm.Scope) error { + scope.SetColumn("ID", uuid.New()) + return nil +} +``` + +### Extra Creating option + +```go +// Add extra SQL option for inserting SQL +db.Set("gorm:insert_option", "ON CONFLICT").Create(&product) +// INSERT INTO products (name, code) VALUES ("name", "code") ON CONFLICT; diff --git a/doc/curd/curd.md b/doc/curd/curd.md new file mode 100644 index 00000000..dd3f4dae --- /dev/null +++ b/doc/curd/curd.md @@ -0,0 +1,9 @@ +# CRUD: Reading and Writing Data + +{% for section in book.chapters["curd/curd.md"].sections %} +* [**{{section.name}}**](../{{section.path}}) +{% if section["sections"] %}{% for subsection in section.sections %} + * [**{{ subsection.name }}**]({{ subsection.path }}) +{% endfor %}{% endif %} +{% endfor %} + diff --git a/doc/curd/delete.md b/doc/curd/delete.md new file mode 100644 index 00000000..353560d4 --- /dev/null +++ b/doc/curd/delete.md @@ -0,0 +1,44 @@ +# Delete + +```go +// Delete an existing record +db.Delete(&email) +//// DELETE from emails where id=10; + +// Add extra SQL option for deleting SQL +db.Set("gorm:delete_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Delete(&email) +//// DELETE from emails where id=10 OPTION (OPTIMIZE FOR UNKNOWN); +``` + +### Batch Delete + +```go +db.Where("email LIKE ?", "%jinzhu%").Delete(Email{}) +//// DELETE from emails where email LIKE "%jinhu%"; +``` + +### Soft Delete + +If struct has `DeletedAt` field, it will get soft delete ability automatically! +Then it won't be deleted from database permanently when call `Delete`. + +```go +db.Delete(&user) +//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111; + +// Batch Delete +db.Where("age = ?", 20).Delete(&User{}) +//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20; + +// Soft deleted records will be ignored when query them +db.Where("age = 20").Find(&user) +//// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL; + +// Find soft deleted records with Unscoped +db.Unscoped().Where("age = 20").Find(&users) +//// SELECT * FROM users WHERE age = 20; + +// Delete record permanently with Unscoped +db.Unscoped().Delete(&order) +//// DELETE FROM orders WHERE id=10; +``` diff --git a/doc/curd/preloading.md b/doc/curd/preloading.md new file mode 100644 index 00000000..dec4f012 --- /dev/null +++ b/doc/curd/preloading.md @@ -0,0 +1,40 @@ +# Preloading (Eager loading) + +```go +db.Preload("Orders").Find(&users) +//// SELECT * FROM users; +//// SELECT * FROM orders WHERE user_id IN (1,2,3,4); + +db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users) +//// SELECT * FROM users; +//// SELECT * FROM orders WHERE user_id IN (1,2,3,4) AND state NOT IN ('cancelled'); + +db.Where("state = ?", "active").Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users) +//// SELECT * FROM users WHERE state = 'active'; +//// SELECT * FROM orders WHERE user_id IN (1,2) AND state NOT IN ('cancelled'); + +db.Preload("Orders").Preload("Profile").Preload("Role").Find(&users) +//// SELECT * FROM users; +//// SELECT * FROM orders WHERE user_id IN (1,2,3,4); // has many +//// SELECT * FROM profiles WHERE user_id IN (1,2,3,4); // has one +//// SELECT * FROM roles WHERE id IN (4,5,6); // belongs to +``` + +#### Custom Preloading SQL + +You could custom preloading SQL by passing in `func(db *gorm.DB) *gorm.DB` (same type as the one used for [Scopes](#scopes)), for example: + +```go +db.Preload("Orders", func(db *gorm.DB) *gorm.DB { + return db.Order("orders.amount DESC") +}).Find(&users) +//// SELECT * FROM users; +//// SELECT * FROM orders WHERE user_id IN (1,2,3,4) order by orders.amount DESC; +``` + +#### Nested Preloading + +```go +db.Preload("Orders.OrderItems").Find(&users) +db.Preload("Orders", "state = ?", "paid").Preload("Orders.OrderItems").Find(&users) +``` diff --git a/doc/curd/query.md b/doc/curd/query.md new file mode 100644 index 00000000..0795c875 --- /dev/null +++ b/doc/curd/query.md @@ -0,0 +1,426 @@ +# Query + +```go +// Get the first record +db.First(&user) +//// SELECT * FROM users ORDER BY id LIMIT 1; + +// Get the last record +db.Last(&user) +//// SELECT * FROM users ORDER BY id DESC LIMIT 1; + +// Get all records +db.Find(&users) +//// SELECT * FROM users; + +// Get record with primary key +db.First(&user, 10) +//// SELECT * FROM users WHERE id = 10; +``` + +## Query With Where (Plain SQL) + +```go +// Get the first matched record +db.Where("name = ?", "jinzhu").First(&user) +//// SELECT * FROM users WHERE name = 'jinzhu' limit 1; + +// Get all matched records +db.Where("name = ?", "jinzhu").Find(&users) +//// SELECT * FROM users WHERE name = 'jinzhu'; + +db.Where("name <> ?", "jinzhu").Find(&users) + +// IN +db.Where("name in (?)", []string{"jinzhu", "jinzhu 2"}).Find(&users) + +// LIKE +db.Where("name LIKE ?", "%jin%").Find(&users) + +// AND +db.Where("name = ? and age >= ?", "jinzhu", "22").Find(&users) + +// Time +db.Where("updated_at > ?", lastWeek).Find(&users) + +db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users) +``` + +## Query With Where (Struct & Map) + +```go +// Struct +db.Where(&User{Name: "jinzhu", Age: 20}).First(&user) +//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 LIMIT 1; + +// Map +db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users) +//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20; + +// Slice of primary keys +db.Where([]int64{20, 21, 22}).Find(&users) +//// SELECT * FROM users WHERE id IN (20, 21, 22); +``` + +## Query With Not + +```go +db.Not("name", "jinzhu").First(&user) +//// SELECT * FROM users WHERE name <> "jinzhu" LIMIT 1; + +// Not In +db.Not("name", []string{"jinzhu", "jinzhu 2"}).Find(&users) +//// SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2"); + +// Not In slice of primary keys +db.Not([]int64{1,2,3}).First(&user) +//// SELECT * FROM users WHERE id NOT IN (1,2,3); + +db.Not([]int64{}).First(&user) +//// SELECT * FROM users; + +// Plain SQL +db.Not("name = ?", "jinzhu").First(&user) +//// SELECT * FROM users WHERE NOT(name = "jinzhu"); + +// Struct +db.Not(User{Name: "jinzhu"}).First(&user) +//// SELECT * FROM users WHERE name <> "jinzhu"; +``` + +## Query With Inline Condition + +```go +// Get by primary key +db.First(&user, 23) +//// SELECT * FROM users WHERE id = 23 LIMIT 1; + +// Plain SQL +db.Find(&user, "name = ?", "jinzhu") +//// SELECT * FROM users WHERE name = "jinzhu"; + +db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20) +//// SELECT * FROM users WHERE name <> "jinzhu" AND age > 20; + +// Struct +db.Find(&users, User{Age: 20}) +//// SELECT * FROM users WHERE age = 20; + +// Map +db.Find(&users, map[string]interface{}{"age": 20}) +//// SELECT * FROM users WHERE age = 20; +``` + +## Query With Or + +```go +db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users) +//// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin'; + +// Struct +db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2"}).Find(&users) +//// SELECT * FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'; + +// Map +db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2"}).Find(&users) +``` + +## Query Chains + +Gorm has a chainable API, you could use it like this + +```go +db.Where("name <> ?","jinzhu").Where("age >= ? and role <> ?",20,"admin").Find(&users) +//// SELECT * FROM users WHERE name <> 'jinzhu' AND age >= 20 AND role <> 'admin'; + +db.Where("role = ?", "admin").Or("role = ?", "super_admin").Not("name = ?", "jinzhu").Find(&users) +``` + +## Extra Querying option + +```go +// Add extra SQL option for selecting SQL +db.Set("gorm:query_option", "FOR UPDATE").First(&user, 10) +//// SELECT * FROM users WHERE id = 10 FOR UPDATE; +``` + +## FirstOrInit + +Get the first matched record, or initialize a record with search conditions. + +```go +// Unfound +db.FirstOrInit(&user, User{Name: "non_existing"}) +//// user -> User{Name: "non_existing"} + +// Found +db.Where(User{Name: "Jinzhu"}).FirstOrInit(&user) +//// user -> User{Id: 111, Name: "Jinzhu", Age: 20} +db.FirstOrInit(&user, map[string]interface{}{"name": "jinzhu"}) +//// user -> User{Id: 111, Name: "Jinzhu", Age: 20} +``` + +### Attrs + +Ignore some values when searching, but use them to initialize the struct if record is not found. + +```go +// Unfound +db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrInit(&user) +//// SELECT * FROM USERS WHERE name = 'non_existing'; +//// user -> User{Name: "non_existing", Age: 20} + +db.Where(User{Name: "noexisting_user"}).Attrs("age", 20).FirstOrInit(&user) +//// SELECT * FROM USERS WHERE name = 'non_existing'; +//// user -> User{Name: "non_existing", Age: 20} + +// Found +db.Where(User{Name: "Jinzhu"}).Attrs(User{Age: 30}).FirstOrInit(&user) +//// SELECT * FROM USERS WHERE name = jinzhu'; +//// user -> User{Id: 111, Name: "Jinzhu", Age: 20} +``` + +### Assign + +Ignore some values when searching, but assign it to the result regardless it is found or not. + +```go +// Unfound +db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrInit(&user) +//// user -> User{Name: "non_existing", Age: 20} + +// Found +db.Where(User{Name: "Jinzhu"}).Assign(User{Age: 30}).FirstOrInit(&user) +//// SELECT * FROM USERS WHERE name = jinzhu'; +//// user -> User{Id: 111, Name: "Jinzhu", Age: 30} +``` + +## FirstOrCreate + +Get the first matched record, or create with search conditions. + +```go +// Unfound +db.FirstOrCreate(&user, User{Name: "non_existing"}) +//// INSERT INTO "users" (name) VALUES ("non_existing"); +//// user -> User{Id: 112, Name: "non_existing"} + +// Found +db.Where(User{Name: "Jinzhu"}).FirstOrCreate(&user) +//// user -> User{Id: 111, Name: "Jinzhu"} +``` + +### Attrs + +Ignore some values when searching, but use them to create the struct if record is not found. like `FirstOrInit` + +```go +// Unfound +db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrCreate(&user) +//// SELECT * FROM users WHERE name = 'non_existing'; +//// INSERT INTO "users" (name, age) VALUES ("non_existing", 20); +//// user -> User{Id: 112, Name: "non_existing", Age: 20} + +// Found +db.Where(User{Name: "jinzhu"}).Attrs(User{Age: 30}).FirstOrCreate(&user) +//// SELECT * FROM users WHERE name = 'jinzhu'; +//// user -> User{Id: 111, Name: "jinzhu", Age: 20} +``` + +### Assign + +Ignore some values when searching, but assign it to the record regardless it is found or not, then save back to database. like `FirstOrInit` + +```go +// Unfound +db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrCreate(&user) +//// SELECT * FROM users WHERE name = 'non_existing'; +//// INSERT INTO "users" (name, age) VALUES ("non_existing", 20); +//// user -> User{Id: 112, Name: "non_existing", Age: 20} + +// Found +db.Where(User{Name: "jinzhu"}).Assign(User{Age: 30}).FirstOrCreate(&user) +//// SELECT * FROM users WHERE name = 'jinzhu'; +//// UPDATE users SET age=30 WHERE id = 111; +//// user -> User{Id: 111, Name: "jinzhu", Age: 30} +``` + +## Select + +```go +db.Select("name, age").Find(&users) +//// SELECT name, age FROM users; + +db.Select([]string{"name", "age"}).Find(&users) +//// SELECT name, age FROM users; + +db.Table("users").Select("COALESCE(age,?)", 42).Rows() +//// SELECT COALESCE(age,'42') FROM users; +``` + +## Order + +```go +db.Order("age desc, name").Find(&users) +//// SELECT * FROM users ORDER BY age desc, name; + +// Multiple orders +db.Order("age desc").Order("name").Find(&users) +//// SELECT * FROM users ORDER BY age desc, name; + +// ReOrder +db.Order("age desc").Find(&users1).Order("age", true).Find(&users2) +//// SELECT * FROM users ORDER BY age desc; (users1) +//// SELECT * FROM users ORDER BY age; (users2) +``` + +## Limit + +```go +db.Limit(3).Find(&users) +//// SELECT * FROM users LIMIT 3; + +// Cancel limit condition with -1 +db.Limit(10).Find(&users1).Limit(-1).Find(&users2) +//// SELECT * FROM users LIMIT 10; (users1) +//// SELECT * FROM users; (users2) +``` + +## Offset + +```go +db.Offset(3).Find(&users) +//// SELECT * FROM users OFFSET 3; + +// Cancel offset condition with -1 +db.Offset(10).Find(&users1).Offset(-1).Find(&users2) +//// SELECT * FROM users OFFSET 10; (users1) +//// SELECT * FROM users; (users2) +``` + +## Count + +```go +db.Where("name = ?", "jinzhu").Or("name = ?", "jinzhu 2").Find(&users).Count(&count) +//// SELECT * from USERS WHERE name = 'jinzhu' OR name = 'jinzhu 2'; (users) +//// SELECT count(*) FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'; (count) + +db.Model(&User{}).Where("name = ?", "jinzhu").Count(&count) +//// SELECT count(*) FROM users WHERE name = 'jinzhu'; (count) + +db.Table("deleted_users").Count(&count) +//// SELECT count(*) FROM deleted_users; +``` + +## Group & Having + +```go +rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Rows() +for rows.Next() { + ... +} + +rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Rows() +for rows.Next() { + ... +} + +type Result struct { + Date time.Time + Total int64 +} +db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Scan(&results) +``` + +## Joins + +```go +rows, err := db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Rows() +for rows.Next() { + ... +} + +db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results) + +// multiple joins with parameter +db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?", "411111111111").Find(&user) +``` + +## Pluck + +Get selected attributes as map + +```go +var ages []int64 +db.Find(&users).Pluck("age", &ages) + +var names []string +db.Model(&User{}).Pluck("name", &names) + +db.Table("deleted_users").Pluck("name", &names) + +// Requesting more than one column? Do it like this: +db.Select("name, age").Find(&users) +``` + +## Scan + +Scan results into another struct. + +```go +type Result struct { + Name string + Age int +} + +var result Result +db.Table("users").Select("name, age").Where("name = ?", 3).Scan(&result) + +// Raw SQL +db.Raw("SELECT name, age FROM users WHERE name = ?", 3).Scan(&result) +``` + +## Scopes + +```go +func AmountGreaterThan1000(db *gorm.DB) *gorm.DB { + return db.Where("amount > ?", 1000) +} + +func PaidWithCreditCard(db *gorm.DB) *gorm.DB { + return db.Where("pay_mode_sign = ?", "C") +} + +func PaidWithCod(db *gorm.DB) *gorm.DB { + return db.Where("pay_mode_sign = ?", "C") +} + +func OrderStatus(status []string) func (db *gorm.DB) *gorm.DB { + return func (db *gorm.DB) *gorm.DB { + return db.Scopes(AmountGreaterThan1000).Where("status in (?)", status) + } +} + +db.Scopes(AmountGreaterThan1000, PaidWithCreditCard).Find(&orders) +// Find all credit card orders and amount greater than 1000 + +db.Scopes(AmountGreaterThan1000, PaidWithCod).Find(&orders) +// Find all COD orders and amount greater than 1000 + +db.Scopes(OrderStatus([]string{"paid", "shipped"})).Find(&orders) +// Find all paid, shipped orders +``` + +## Specifying The Table Name + +```go +// Create `deleted_users` table with struct User's definition +db.Table("deleted_users").CreateTable(&User{}) + +var deleted_users []User +db.Table("deleted_users").Find(&deleted_users) +//// SELECT * FROM deleted_users; + +db.Table("deleted_users").Where("name = ?", "jinzhu").Delete() +//// DELETE FROM deleted_users WHERE name = 'jinzhu'; +``` diff --git a/doc/curd/update.md b/doc/curd/update.md new file mode 100644 index 00000000..4f99b77f --- /dev/null +++ b/doc/curd/update.md @@ -0,0 +1,120 @@ +# Update + +### Update All Fields + +`Save` will include all fields when perform the Updating SQL, even it is not changed + +```go +db.First(&user) + +user.Name = "jinzhu 2" +user.Age = 100 +db.Save(&user) + +//// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111; +``` + +### Update Changed Fields + +If you only want to update changed Fields, you could use `Update`, `Updates` + +```go +// Update single attribute if it is changed +db.Model(&user).Update("name", "hello") +//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111; + +// Update single attribute with combined conditions +db.Model(&user).Where("active = ?", true).Update("name", "hello") +//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true; + + +// Update multiple attributes with `map`, will only update those changed fields +db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) +//// UPDATE users SET name='hello', age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111; + +// Update multiple attributes with `struct`, will only update those changed & non blank fields +db.Model(&user).Updates(User{Name: "hello", Age: 18}) +//// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111; + +// WARNING when update with struct, GORM will only update those fields that with non blank value +// For below Update, nothing will be updated as "", 0, false are blank values of their types +db.Model(&user).Updates(User{Name: "", Age: 0, Actived: false}) +``` + +### Update Selected Fields + +If you only want to update or ignore some fields when updating, you could use `Select`, `Omit` + +```go +db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) +//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111; + +db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) +//// UPDATE users SET age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111; +``` + +### Update Changed Fields Without Callbacks + +Updating operations above will invoke `BeforeUpdate`, `AfterUpdate`, Update UpdatedAt timestamp, Save Associations callbacks, if you don't call them, you could use `UpdateColumn`, `UpdateColumns` + +```go +// Update single attribute, similar with `Update` +db.Model(&user).UpdateColumn("name", "hello") +//// UPDATE users SET name='hello' WHERE id = 111; + +// Update multiple attributes, similar with `Updates` +db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18}) +//// UPDATE users SET name='hello', age=18 WHERE id = 111; +``` + +### Batch Updates + +Callbacks won't run when do batch updates + +```go +db.Table("users").Where("id IN (?)", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18}) +//// UPDATE users SET name='hello', age=18 WHERE id IN (10, 11); + +// Update with struct only works with none zero values, or use map[string]interface{} +db.Model(User{}).Updates(User{Name: "hello", Age: 18}) +//// UPDATE users SET name='hello', age=18; + +// Get updated records count with `RowsAffected` +db.Model(User{}).Updates(User{Name: "hello", Age: 18}).RowsAffected +``` + +### Update with SQL Expression + +```go +DB.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100)) +//// UPDATE "products" SET "price" = price * '2' + '100', "updated_at" = '2013-11-17 21:34:10' WHERE "id" = '2'; + +DB.Model(&product).Updates(map[string]interface{}{"price": gorm.Expr("price * ? + ?", 2, 100)}) +//// UPDATE "products" SET "price" = price * '2' + '100', "updated_at" = '2013-11-17 21:34:10' WHERE "id" = '2'; + +DB.Model(&product).UpdateColumn("quantity", gorm.Expr("quantity - ?", 1)) +//// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = '2'; + +DB.Model(&product).Where("quantity > 1").UpdateColumn("quantity", gorm.Expr("quantity - ?", 1)) +//// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = '2' AND quantity > 1; +``` + +### Change Updating Values In Callbacks + +If you want to change updating values in callbacks using `BeforeUpdate`, `BeforeSave`, you could use `scope.SetColumn`, for example: + +```go +func (user *User) BeforeSave(scope *gorm.Scope) (err error) { + if pw, err := bcrypt.GenerateFromPassword(user.Password, 0); err == nil { + scope.SetColumn("EncryptedPassword", pw) + } +} +``` + +### Extra Updating option + +```go +// Add extra SQL option for updating SQL +db.Model(&user).Set("gorm:update_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Update("name, "hello") +//// UPDATE users SET name='hello', updated_at = '2013-11-17 21:34:10' WHERE id=111 OPTION (OPTIMIZE FOR UNKNOWN); +``` diff --git a/doc/development.md b/doc/development/development.md similarity index 100% rename from doc/development.md rename to doc/development/development.md diff --git a/doc/models/defination.md b/doc/models/defination.md index 82106bde..8ae3c726 100644 --- a/doc/models/defination.md +++ b/doc/models/defination.md @@ -6,20 +6,31 @@ ```go type User struct { - ID int + gorm.Model Birthday time.Time Age int Name string `sql:"size:255"` // Default size for string is 255, you could reset it with this tag Num int `sql:"AUTO_INCREMENT"` - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt *time.Time IgnoreMe int `sql:"-"` // Ignore this field } ``` ## Conventions & Overriding Conventions +### `gorm.Model` struct + +Gorm has defined struct `gorm.Model`, which could be embeded in your models, it will add fields `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt` to your model + +```go +// Model's definition +type Model struct { + ID uint `gorm:"primary_key"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt *time.Time +} +``` + ### Table name is the pluralized version of struct name ```go @@ -30,8 +41,16 @@ type (User) TableName() string { return "profiles" } -// Or disable table name's pluralization globally -db.SingularTable(true) // if set this to true, then default table name will be `user`, table name setted with `TableName` won't be affected +func (u User) TableName() string { + if u.Role == "admin" { + return "admin_users" + } else { + return "users" + } +} + +// Disable table name's pluralization globally +db.SingularTable(true) // if set this to true, `User`'s default table name will be `user`, table name setted with `TableName` won't be affected ``` ### Column name is the snake case of field's name @@ -45,37 +64,48 @@ type User struct { } type Animal struct { - AnimalId int64 `gorm:"column:beast_id"` // set column name to be `beast_id` - Birthday time.Time `gorm:"column:day_of_the_beast"` // set column name to be `day_of_the_beast` - Age int64 `gorm:"column:age_of_the_beast"` // set column name to be `age_of_the_beast` + AnimalId int64 `gorm:"column:beast_id"` // set column name to `beast_id` + Birthday time.Time `gorm:"column:day_of_the_beast"` // set column name to `day_of_the_beast` + Age int64 `gorm:"column:age_of_the_beast"` // set column name to `age_of_the_beast` } ``` -### Default use field `ID` as primary key +### Field `ID` as primary key ```go type User struct { - ID uint // field `ID` is the default primary key for `User` + ID uint // field named `ID` is the default primary key for `User` Name string } +// your could also use tag `primary_key` to set other field as primary key type Animal struct { - // tag `primary_key` used to set `AnimalId` to be primary key - AnimalId int64 `gorm:"primary_key"` + AnimalId int64 `gorm:"primary_key"` // set AnimalId to be primary key Name string Age int64 } ``` -### Use `CreatedAt` to store record's created time if field exists +### Field `CreatedAt` used to store record's created time + +Create records having `CreatedAt` field will set it to current time. ```go -db.Create(&user) // will set field `CreatedAt`'s time to time now +db.Create(&user) // will set `CreatedAt` to current time -// If you want to change its value, use `Update` +// To change its value, you could use `Update` db.Model(&user).Update("CreatedAt", time.Now()) ``` -### Use `UpdatedAt` to store record's updated time if field exists +### Use `UpdatedAt` used to store record's updated time + +Save records having `UpdatedAt` field will set it to current time. + +```go +db.Save(&user) // will set `UpdatedAt` to current time +db.Model(&user).Update("name", "jinzhu") // will set `UpdatedAt` to current time +``` + ### Use `DeletedAt` to store record's deleted time if field exists -### Gorm provide a default model struct, you could embed it in your struct + +Delete records having `DeletedAt` field, it won't delete the record from database, but will set field `DeletedAt`'s value to current time. From 9d0203847c85648a506d1c7fe783557e75dee664 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Thu, 25 Feb 2016 22:58:56 +0800 Subject: [PATCH 64/83] Update README --- README.md | 1411 +---------------- doc/README.md | 31 +- doc/advanced/advanced.md | 10 + .../{transaction.md => transactions.md} | 3 +- doc/book.json | 2 +- doc/curd/create.md | 1 + 6 files changed, 46 insertions(+), 1412 deletions(-) create mode 100644 doc/advanced/advanced.md rename doc/advanced/{transaction.md => transactions.md} (99%) diff --git a/README.md b/README.md index 0a1463c0..a0a18330 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # GORM -[![Join the chat at https://gitter.im/jinzhu/gorm](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jinzhu/gorm?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - The fantastic ORM library for Golang, aims to be developer friendly. [![wercker status](https://app.wercker.com/status/0cb7bb1039e21b74f8274941428e0921/s/master "wercker status")](https://app.wercker.com/project/bykey/0cb7bb1039e21b74f8274941428e0921) +[![GoDoc](https://godoc.org/github.com/jinzhu/gorm?status.svg)](https://godoc.org/github.com/jinzhu/gorm) +[![Join the chat at https://gitter.im/jinzhu/gorm](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jinzhu/gorm?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ## Overview @@ -22,1419 +22,12 @@ The fantastic ORM library for Golang, aims to be developer friendly. * Every feature comes with tests * Developer Friendly -# Getting Started - ## Install ``` go get -u github.com/jinzhu/gorm ``` -## Table of Contents - -- [Define Models (Structs)](#define-models-structs) -- [Conventions](#conventions) -- [Initialize Database](#initialize-database) -- [Migration](#migration) -- [Basic CRUD](#basic-crud) - - [Create](#create-record) - - [Create With Associations](#create-with-associations) - - [Default Values](#default-values) - - [Setting Primary Key In Callbacks](#setting-primary-key-in-callbacks) - - [Extra Creating option](#extra-creating-option) - - [Query](#query) - - [Query With Where (Plain SQL)](#query-with-where-plain-sql) - - [Query With Where (Struct & Map)](#query-with-where-struct--map) - - [Query With Not](#query-with-not) - - [Query With Inline Condition](#query-with-inline-condition) - - [Query With Or](#query-with-or) - - [Query Chains](#query-chains) - - [Preloading (Eager loading)](#preloading-eager-loading) - - [Extra Querying option](#extra-querying-option) - - [Update](#update) - - [Update All Fields](#update-all-fields) - - [Update Changed Fields](#update-changed-fields) - - [Update Selected Fields](#update-selected-fields) - - [Update Changed Fields Without Callbacks](#update-changed-fields-without-callbacks) - - [Batch Updates](#batch-updates) - - [Update with SQL Expression](#update-with-sql-expression) - - [Change Updating Values In Callbacks](#change-updating-values-in-callbacks) - - [Extra Updating option](#extra-updating-option) - - [Delete](#delete) - - [Batch Delete](#batch-delete) - - [Soft Delete](#soft-delete) -- [Associations](#associations) - - [Has One](#has-one) - - [Belongs To](#belongs-to) - - [Has Many](#has-many) - - [Many To Many](#many-to-many) - - [Polymorphism](#polymorphism) - - [Association Mode](#association-mode) -- [Advanced Usage](#advanced-usage) - - [FirstOrInit](#firstorinit) - - [FirstOrCreate](#firstorcreate) - - [Select](#select) - - [Order](#order) - - [Limit](#limit) - - [Offset](#offset) - - [Count](#count) - - [Group & Having](#group--having) - - [Joins](#joins) - - [Transactions](#transactions) - - [Scopes](#scopes) - - [Callbacks](#callbacks) - - [Pluck](#pluck) - - [Scan](#scan) - - [Raw SQL](#raw-sql) - - [Row & Rows](#row--rows) - - [Specifying The Table Name](#specifying-the-table-name) - - [Error Handling](#error-handling) - - [Logger](#logger) - - [Existing Schema](#existing-schema) - - [Composite Primary Key](#composite-primary-key) - - [Database Indexes & Foreign Key](#database-indexes--foreign-key) - - [More examples with query chain](#more-examples-with-query-chain) - -## Define Models (Structs) - -```go -type User struct { - ID int - Birthday time.Time - Age int - Name string `sql:"size:255"` // Default size for string is 255, you could reset it with this tag - Num int `sql:"AUTO_INCREMENT"` - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt *time.Time - - CreditCard CreditCard // One-To-One relationship (has one - use CreditCard's UserID as foreign key) - Emails []Email // One-To-Many relationship (has many - use Email's UserID as foreign key) - - BillingAddress Address // One-To-One relationship (belongs to - use BillingAddressID as foreign key) - BillingAddressID sql.NullInt64 - - ShippingAddress Address // One-To-One relationship (belongs to - use ShippingAddressID as foreign key) - ShippingAddressID int - - IgnoreMe int `sql:"-"` // Ignore this field - Languages []Language `gorm:"many2many:user_languages;"` // Many-To-Many relationship, 'user_languages' is join table -} - -type Email struct { - ID int - UserID int `sql:"index"` // Foreign key (belongs to), tag `index` will create index for this field when using AutoMigrate - Email string `sql:"type:varchar(100);unique_index"` // Set field's sql type, tag `unique_index` will create unique index - Subscribed bool -} - -type Address struct { - ID int - Address1 string `sql:"not null;unique"` // Set field as not nullable and unique - Address2 string `sql:"type:varchar(100);unique"` - Post sql.NullString `sql:"not null"` -} - -type Language struct { - ID int - Name string `sql:"index:idx_name_code"` // Create index with name, and will create combined index if find other fields defined same name - Code string `sql:"index:idx_name_code"` // `unique_index` also works -} - -type CreditCard struct { - gorm.Model - UserID uint - Number string -} -``` - -## Conventions - -* Table name is the plural of struct name's snake case, you can disable pluralization with `db.SingularTable(true)`, or [Specifying The Table Name For A Struct Permanently With TableName](#specifying-the-table-name-for-a-struct-permanently-with-tablename) - -```go -type User struct{} // struct User's database table name is "users" by default, will be "user" if you disabled pluralisation -``` - -* Column name is the snake case of field's name -* Use `ID` field as primary key -* Use `CreatedAt` to store record's created time if field exists -* Use `UpdatedAt` to store record's updated time if field exists -* Use `DeletedAt` to store record's deleted time if field exists [Soft Delete](#soft-delete) -* Gorm provide a default model struct, you could embed it in your struct - -```go -type Model struct { - ID uint `gorm:"primary_key"` - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt *time.Time -} - -type User struct { - gorm.Model - Name string -} -``` - -## Initialize Database - -```go -import ( - "github.com/jinzhu/gorm" - _ "github.com/lib/pq" - _ "github.com/go-sql-driver/mysql" - _ "github.com/mattn/go-sqlite3" -) - -db, err := gorm.Open("postgres", "user=gorm dbname=gorm sslmode=disable") -// db, err := gorm.Open("foundation", "dbname=gorm") // FoundationDB. -// db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local") -// db, err := gorm.Open("sqlite3", "/tmp/gorm.db") - -// You can also use an existing database connection handle -// dbSql, _ := sql.Open("postgres", "user=gorm dbname=gorm sslmode=disable") -// db, _ := gorm.Open("postgres", dbSql) - -// Get database connection handle [*sql.DB](http://golang.org/pkg/database/sql/#DB) -db.DB() - -// Then you could invoke `*sql.DB`'s functions with it -db.DB().Ping() -db.DB().SetMaxIdleConns(10) -db.DB().SetMaxOpenConns(100) - -// Disable table name's pluralization -db.SingularTable(true) -``` - -## Migration - -```go -// Create table -db.CreateTable(&User{}) -db.Set("gorm:table_options", "ENGINE=InnoDB").CreateTable(&User{}) - -// Drop table -db.DropTable(&User{}) - -// ModifyColumn -db.Model(&User{}).ModifyColumn("description", "text") - -// DropColumn -db.Model(&User{}).DropColumn("description") - -// Automating Migration -db.AutoMigrate(&User{}) -db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{}) -db.AutoMigrate(&User{}, &Product{}, &Order{}) -// Feel free to change your struct, AutoMigrate will keep your database up-to-date. -// AutoMigrate will ONLY add *new columns* and *new indexes*, -// WON'T update current column's type or delete unused columns, to protect your data. -// If the table is not existing, AutoMigrate will create the table automatically. -``` - -# Basic CRUD - -## Create Record - -```go -user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()} - -db.NewRecord(user) // => returns `true` as primary key is blank - -db.Create(&user) - -db.NewRecord(user) // => return `false` after `user` created - -// Associations will be inserted automatically when save the record -user := User{ - Name: "jinzhu", - BillingAddress: Address{Address1: "Billing Address - Address 1"}, - ShippingAddress: Address{Address1: "Shipping Address - Address 1"}, - Emails: []Email{{Email: "jinzhu@example.com"}, {Email: "jinzhu-2@example@example.com"}}, - Languages: []Language{{Name: "ZH"}, {Name: "EN"}}, -} - -db.Create(&user) -//// BEGIN TRANSACTION; -//// INSERT INTO "addresses" (address1) VALUES ("Billing Address - Address 1"); -//// INSERT INTO "addresses" (address1) VALUES ("Shipping Address - Address 1"); -//// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2); -//// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com"); -//// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu-2@example.com"); -//// INSERT INTO "languages" ("name") VALUES ('ZH'); -//// INSERT INTO user_languages ("user_id","language_id") VALUES (111, 1); -//// INSERT INTO "languages" ("name") VALUES ('EN'); -//// INSERT INTO user_languages ("user_id","language_id") VALUES (111, 2); -//// COMMIT; -``` - -### Create With Associations - -Refer [Associations](#associations) for more details - -### Default Values - -You could defined default value in the `sql` tag, then the generated creating SQL will ignore these fields that including default value and its value is blank, and after inserted the record into databae, gorm will load those fields's value from database. - -```go -type Animal struct { - ID int64 - Name string `sql:"default:'galeone'"` - Age int64 -} - -var animal = Animal{Age: 99, Name: ""} -db.Create(&animal) -// INSERT INTO animals("age") values('99'); -// SELECT name from animals WHERE ID=111; // the returning primary key is 111 -// animal.Name => 'galeone' -``` - -### Setting Primary Key In Callbacks - -If you want to set primary key in `BeforeCreate` callback, you could use `scope.SetColumn`, for example: - -```go -func (user *User) BeforeCreate(scope *gorm.Scope) error { - scope.SetColumn("ID", uuid.New()) - return nil -} -``` - -### Extra Creating option - -```go -// Add extra SQL option for inserting SQL -db.Set("gorm:insert_option", "ON CONFLICT").Create(&product) -// INSERT INTO products (name, code) VALUES ("name", "code") ON CONFLICT; -``` - -## Query - -```go -// Get the first record -db.First(&user) -//// SELECT * FROM users ORDER BY id LIMIT 1; - -// Get the last record -db.Last(&user) -//// SELECT * FROM users ORDER BY id DESC LIMIT 1; - -// Get all records -db.Find(&users) -//// SELECT * FROM users; - -// Get record with primary key -db.First(&user, 10) -//// SELECT * FROM users WHERE id = 10; -``` - -### Query With Where (Plain SQL) - -```go -// Get the first matched record -db.Where("name = ?", "jinzhu").First(&user) -//// SELECT * FROM users WHERE name = 'jinzhu' limit 1; - -// Get all matched records -db.Where("name = ?", "jinzhu").Find(&users) -//// SELECT * FROM users WHERE name = 'jinzhu'; - -db.Where("name <> ?", "jinzhu").Find(&users) - -// IN -db.Where("name in (?)", []string{"jinzhu", "jinzhu 2"}).Find(&users) - -// LIKE -db.Where("name LIKE ?", "%jin%").Find(&users) - -// AND -db.Where("name = ? and age >= ?", "jinzhu", "22").Find(&users) - -// Time -db.Where("updated_at > ?", lastWeek).Find(&users) - -db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users) -``` - -### Query With Where (Struct & Map) - -```go -// Struct -db.Where(&User{Name: "jinzhu", Age: 20}).First(&user) -//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 LIMIT 1; - -// Map -db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users) -//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20; - -// Slice of primary keys -db.Where([]int64{20, 21, 22}).Find(&users) -//// SELECT * FROM users WHERE id IN (20, 21, 22); -``` - -### Query With Not - -```go -db.Not("name", "jinzhu").First(&user) -//// SELECT * FROM users WHERE name <> "jinzhu" LIMIT 1; - -// Not In -db.Not("name", []string{"jinzhu", "jinzhu 2"}).Find(&users) -//// SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2"); - -// Not In slice of primary keys -db.Not([]int64{1,2,3}).First(&user) -//// SELECT * FROM users WHERE id NOT IN (1,2,3); - -db.Not([]int64{}).First(&user) -//// SELECT * FROM users; - -// Plain SQL -db.Not("name = ?", "jinzhu").First(&user) -//// SELECT * FROM users WHERE NOT(name = "jinzhu"); - -// Struct -db.Not(User{Name: "jinzhu"}).First(&user) -//// SELECT * FROM users WHERE name <> "jinzhu"; -``` - -### Query With Inline Condition - -```go -// Get by primary key -db.First(&user, 23) -//// SELECT * FROM users WHERE id = 23 LIMIT 1; - -// Plain SQL -db.Find(&user, "name = ?", "jinzhu") -//// SELECT * FROM users WHERE name = "jinzhu"; - -db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20) -//// SELECT * FROM users WHERE name <> "jinzhu" AND age > 20; - -// Struct -db.Find(&users, User{Age: 20}) -//// SELECT * FROM users WHERE age = 20; - -// Map -db.Find(&users, map[string]interface{}{"age": 20}) -//// SELECT * FROM users WHERE age = 20; -``` - -### Query With Or - -```go -db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users) -//// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin'; - -// Struct -db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2"}).Find(&users) -//// SELECT * FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'; - -// Map -db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2"}).Find(&users) -``` - -### Query Chains - -Gorm has a chainable API, you could use it like this - -```go -db.Where("name <> ?","jinzhu").Where("age >= ? and role <> ?",20,"admin").Find(&users) -//// SELECT * FROM users WHERE name <> 'jinzhu' AND age >= 20 AND role <> 'admin'; - -db.Where("role = ?", "admin").Or("role = ?", "super_admin").Not("name = ?", "jinzhu").Find(&users) -``` - -### Preloading (Eager loading) - -```go -db.Preload("Orders").Find(&users) -//// SELECT * FROM users; -//// SELECT * FROM orders WHERE user_id IN (1,2,3,4); - -db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users) -//// SELECT * FROM users; -//// SELECT * FROM orders WHERE user_id IN (1,2,3,4) AND state NOT IN ('cancelled'); - -db.Where("state = ?", "active").Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users) -//// SELECT * FROM users WHERE state = 'active'; -//// SELECT * FROM orders WHERE user_id IN (1,2) AND state NOT IN ('cancelled'); - -db.Preload("Orders").Preload("Profile").Preload("Role").Find(&users) -//// SELECT * FROM users; -//// SELECT * FROM orders WHERE user_id IN (1,2,3,4); // has many -//// SELECT * FROM profiles WHERE user_id IN (1,2,3,4); // has one -//// SELECT * FROM roles WHERE id IN (4,5,6); // belongs to -``` - -#### Custom Preloading SQL - -You could custom preloading SQL by passing in `func(db *gorm.DB) *gorm.DB` (same type as the one used for [Scopes](#scopes)), for example: - -```go -db.Preload("Orders", func(db *gorm.DB) *gorm.DB { - return db.Order("orders.amount DESC") -}).Find(&users) -//// SELECT * FROM users; -//// SELECT * FROM orders WHERE user_id IN (1,2,3,4) order by orders.amount DESC; -``` - -#### Nested Preloading - -```go -db.Preload("Orders.OrderItems").Find(&users) -db.Preload("Orders", "state = ?", "paid").Preload("Orders.OrderItems").Find(&users) -``` - -### Extra Querying option - -```go -// Add extra SQL option for selecting SQL -db.Set("gorm:query_option", "FOR UPDATE").First(&user, 10) -//// SELECT * FROM users WHERE id = 10 FOR UPDATE; -``` - -## Update - -### Update All Fields - -`Save` will include all fields when perform the Updating SQL, even it is not changed - -```go -db.First(&user) - -user.Name = "jinzhu 2" -user.Age = 100 -db.Save(&user) - -//// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111; -``` - -### Update Changed Fields - -If you only want to update changed Fields, you could use `Update`, `Updates` - -```go -// Update single attribute if it is changed -db.Model(&user).Update("name", "hello") -//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111; - -// Update single attribute with combined conditions -db.Model(&user).Where("active = ?", true).Update("name", "hello") -//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true; - - -// Update multiple attributes with `map`, will only update those changed fields -db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) -//// UPDATE users SET name='hello', age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111; - -// Update multiple attributes with `struct`, will only update those changed & non blank fields -db.Model(&user).Updates(User{Name: "hello", Age: 18}) -//// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111; - -// WARNING when update with struct, GORM will only update those fields that with non blank value -// For below Update, nothing will be updated as "", 0, false are blank values of their types -db.Model(&user).Updates(User{Name: "", Age: 0, Actived: false}) -``` - -### Update Selected Fields - -If you only want to update or ignore some fields when updating, you could use `Select`, `Omit` - -```go -db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) -//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111; - -db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) -//// UPDATE users SET age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111; -``` - -### Update Changed Fields Without Callbacks - -Updating operations above will invoke `BeforeUpdate`, `AfterUpdate`, Update UpdatedAt timestamp, Save Associations callbacks, if you don't call them, you could use `UpdateColumn`, `UpdateColumns` - -```go -// Update single attribute, similar with `Update` -db.Model(&user).UpdateColumn("name", "hello") -//// UPDATE users SET name='hello' WHERE id = 111; - -// Update multiple attributes, similar with `Updates` -db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18}) -//// UPDATE users SET name='hello', age=18 WHERE id = 111; -``` - -### Batch Updates - -Callbacks won't run when do batch updates - -```go -db.Table("users").Where("id IN (?)", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18}) -//// UPDATE users SET name='hello', age=18 WHERE id IN (10, 11); - -// Update with struct only works with none zero values, or use map[string]interface{} -db.Model(User{}).Updates(User{Name: "hello", Age: 18}) -//// UPDATE users SET name='hello', age=18; - -// Get updated records count with `RowsAffected` -db.Model(User{}).Updates(User{Name: "hello", Age: 18}).RowsAffected -``` - -### Update with SQL Expression - -```go -DB.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100)) -//// UPDATE "products" SET "price" = price * '2' + '100', "updated_at" = '2013-11-17 21:34:10' WHERE "id" = '2'; - -DB.Model(&product).Updates(map[string]interface{}{"price": gorm.Expr("price * ? + ?", 2, 100)}) -//// UPDATE "products" SET "price" = price * '2' + '100', "updated_at" = '2013-11-17 21:34:10' WHERE "id" = '2'; - -DB.Model(&product).UpdateColumn("quantity", gorm.Expr("quantity - ?", 1)) -//// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = '2'; - -DB.Model(&product).Where("quantity > 1").UpdateColumn("quantity", gorm.Expr("quantity - ?", 1)) -//// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = '2' AND quantity > 1; -``` - -### Change Updating Values In Callbacks - -If you want to change updating values in callbacks using `BeforeUpdate`, `BeforeSave`, you could use `scope.SetColumn`, for example: - -```go -func (user *User) BeforeSave(scope *gorm.Scope) (err error) { - if pw, err := bcrypt.GenerateFromPassword(user.Password, 0); err == nil { - scope.SetColumn("EncryptedPassword", pw) - } -} -``` - -### Extra Updating option - -```go -// Add extra SQL option for updating SQL -db.Model(&user).Set("gorm:update_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Update("name, "hello") -//// UPDATE users SET name='hello', updated_at = '2013-11-17 21:34:10' WHERE id=111 OPTION (OPTIMIZE FOR UNKNOWN); -``` - -## Delete - -```go -// Delete an existing record -db.Delete(&email) -//// DELETE from emails where id=10; - -// Add extra SQL option for deleting SQL -db.Set("gorm:delete_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Delete(&email) -//// DELETE from emails where id=10 OPTION (OPTIMIZE FOR UNKNOWN); -``` - -### Batch Delete - -```go -db.Where("email LIKE ?", "%jinzhu%").Delete(Email{}) -//// DELETE from emails where email LIKE "%jinhu%"; -``` - -### Soft Delete - -If struct has `DeletedAt` field, it will get soft delete ability automatically! -Then it won't be deleted from database permanently when call `Delete`. - -```go -db.Delete(&user) -//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111; - -// Batch Delete -db.Where("age = ?", 20).Delete(&User{}) -//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20; - -// Soft deleted records will be ignored when query them -db.Where("age = 20").Find(&user) -//// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL; - -// Find soft deleted records with Unscoped -db.Unscoped().Where("age = 20").Find(&users) -//// SELECT * FROM users WHERE age = 20; - -// Delete record permanently with Unscoped -db.Unscoped().Delete(&order) -//// DELETE FROM orders WHERE id=10; -``` - -## Associations - -### Has One - -```go -// User has one CreditCard, UserID is the foreign key -type User struct { - gorm.Model - CreditCard CreditCard -} - -type CreditCard struct { - gorm.Model - UserID uint - Number string -} - -var card CreditCard -db.Model(&user).Related(&card, "CreditCard") -//// SELECT * FROM credit_cards WHERE user_id = 123; // 123 is user's primary key -// CreditCard is user's field name, it means get user's CreditCard relations and fill it into variable card -// If the field name is same as the variable's type name, like above example, it could be omitted, like: -db.Model(&user).Related(&card) -``` - -### Belongs To - -```go -// User belongs to a profile, ProfileID is the foreign key -type User struct { - gorm.Model - Profile Profile - ProfileID int -} - -type Profile struct { - gorm.Model - Name string -} - -db.Model(&user).Related(&profile) -//// SELECT * FROM profiles WHERE id = 111; // 111 is user's foreign key ProfileID -``` - -### Has Many - -```go -// User has many emails, UserID is the foreign key -type User struct { - gorm.Model - Emails []Email -} - -type Email struct { - gorm.Model - Email string - UserID uint -} - -db.Model(&user).Related(&emails) -//// SELECT * FROM emails WHERE user_id = 111; // 111 is user's primary key -``` - -### Many To Many - -```go -// User has and belongs to many languages, use `user_languages` as join table -type User struct { - gorm.Model - Languages []Language `gorm:"many2many:user_languages;"` -} - -type Language struct { - gorm.Model - Name string -} - -db.Model(&user).Related(&languages) -//// SELECT * FROM "languages" INNER JOIN "user_languages" ON "user_languages"."language_id" = "languages"."id" WHERE "user_languages"."user_id" = 111 -``` - -### Polymorphism - -Supports polymorphic has-many and has-one associations. - -```go - type Cat struct { - Id int - Name string - Toy Toy `gorm:"polymorphic:Owner;"` - } - - type Dog struct { - Id int - Name string - Toy Toy `gorm:"polymorphic:Owner;"` - } - - type Toy struct { - Id int - Name string - OwnerId int - OwnerType string - } -``` -Note: polymorphic belongs-to and many-to-many are explicitly NOT supported, and will throw errors. - -## Association Mode - -Association Mode contains some helper methods to handle relationship things easily. - -```go -// Start Association Mode -var user User -db.Model(&user).Association("Languages") -// `user` is the source, it need to be a valid record (contains primary key) -// `Languages` is source's field name for a relationship. -// If those conditions not matched, will return an error, check it with: -// db.Model(&user).Association("Languages").Error - - -// Query - Find out all related associations -db.Model(&user).Association("Languages").Find(&languages) - - -// Append - Append new associations for many2many, has_many, will replace current association for has_one, belongs_to -db.Model(&user).Association("Languages").Append([]Language{languageZH, languageEN}) -db.Model(&user).Association("Languages").Append(Language{Name: "DE"}) - - -// Delete - Remove relationship between source & passed arguments, won't delete those arguments -db.Model(&user).Association("Languages").Delete([]Language{languageZH, languageEN}) -db.Model(&user).Association("Languages").Delete(languageZH, languageEN) - - -// Replace - Replace current associations with new one -db.Model(&user).Association("Languages").Replace([]Language{languageZH, languageEN}) -db.Model(&user).Association("Languages").Replace(Language{Name: "DE"}, languageEN) - - -// Count - Return the count of current associations -db.Model(&user).Association("Languages").Count() - - -// Clear - Remove relationship between source & current associations, won't delete those associations -db.Model(&user).Association("Languages").Clear() -``` - -## Advanced Usage - -## FirstOrInit - -Get the first matched record, or initialize a record with search conditions. - -```go -// Unfound -db.FirstOrInit(&user, User{Name: "non_existing"}) -//// user -> User{Name: "non_existing"} - -// Found -db.Where(User{Name: "Jinzhu"}).FirstOrInit(&user) -//// user -> User{Id: 111, Name: "Jinzhu", Age: 20} -db.FirstOrInit(&user, map[string]interface{}{"name": "jinzhu"}) -//// user -> User{Id: 111, Name: "Jinzhu", Age: 20} -``` - -### Attrs - -Ignore some values when searching, but use them to initialize the struct if record is not found. - -```go -// Unfound -db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrInit(&user) -//// SELECT * FROM USERS WHERE name = 'non_existing'; -//// user -> User{Name: "non_existing", Age: 20} - -db.Where(User{Name: "noexisting_user"}).Attrs("age", 20).FirstOrInit(&user) -//// SELECT * FROM USERS WHERE name = 'non_existing'; -//// user -> User{Name: "non_existing", Age: 20} - -// Found -db.Where(User{Name: "Jinzhu"}).Attrs(User{Age: 30}).FirstOrInit(&user) -//// SELECT * FROM USERS WHERE name = jinzhu'; -//// user -> User{Id: 111, Name: "Jinzhu", Age: 20} -``` - -### Assign - -Ignore some values when searching, but assign it to the result regardless it is found or not. - -```go -// Unfound -db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrInit(&user) -//// user -> User{Name: "non_existing", Age: 20} - -// Found -db.Where(User{Name: "Jinzhu"}).Assign(User{Age: 30}).FirstOrInit(&user) -//// SELECT * FROM USERS WHERE name = jinzhu'; -//// user -> User{Id: 111, Name: "Jinzhu", Age: 30} -``` - -## FirstOrCreate - -Get the first matched record, or create with search conditions. - -```go -// Unfound -db.FirstOrCreate(&user, User{Name: "non_existing"}) -//// INSERT INTO "users" (name) VALUES ("non_existing"); -//// user -> User{Id: 112, Name: "non_existing"} - -// Found -db.Where(User{Name: "Jinzhu"}).FirstOrCreate(&user) -//// user -> User{Id: 111, Name: "Jinzhu"} -``` - -### Attrs - -Ignore some values when searching, but use them to create the struct if record is not found. like `FirstOrInit` - -```go -// Unfound -db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrCreate(&user) -//// SELECT * FROM users WHERE name = 'non_existing'; -//// INSERT INTO "users" (name, age) VALUES ("non_existing", 20); -//// user -> User{Id: 112, Name: "non_existing", Age: 20} - -// Found -db.Where(User{Name: "jinzhu"}).Attrs(User{Age: 30}).FirstOrCreate(&user) -//// SELECT * FROM users WHERE name = 'jinzhu'; -//// user -> User{Id: 111, Name: "jinzhu", Age: 20} -``` - -### Assign - -Ignore some values when searching, but assign it to the record regardless it is found or not, then save back to database. like `FirstOrInit` - -```go -// Unfound -db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrCreate(&user) -//// SELECT * FROM users WHERE name = 'non_existing'; -//// INSERT INTO "users" (name, age) VALUES ("non_existing", 20); -//// user -> User{Id: 112, Name: "non_existing", Age: 20} - -// Found -db.Where(User{Name: "jinzhu"}).Assign(User{Age: 30}).FirstOrCreate(&user) -//// SELECT * FROM users WHERE name = 'jinzhu'; -//// UPDATE users SET age=30 WHERE id = 111; -//// user -> User{Id: 111, Name: "jinzhu", Age: 30} -``` - -## Select - -```go -db.Select("name, age").Find(&users) -//// SELECT name, age FROM users; - -db.Select([]string{"name", "age"}).Find(&users) -//// SELECT name, age FROM users; - -db.Table("users").Select("COALESCE(age,?)", 42).Rows() -//// SELECT COALESCE(age,'42') FROM users; -``` - -## Order - -```go -db.Order("age desc, name").Find(&users) -//// SELECT * FROM users ORDER BY age desc, name; - -// Multiple orders -db.Order("age desc").Order("name").Find(&users) -//// SELECT * FROM users ORDER BY age desc, name; - -// ReOrder -db.Order("age desc").Find(&users1).Order("age", true).Find(&users2) -//// SELECT * FROM users ORDER BY age desc; (users1) -//// SELECT * FROM users ORDER BY age; (users2) -``` - -## Limit - -```go -db.Limit(3).Find(&users) -//// SELECT * FROM users LIMIT 3; - -// Cancel limit condition with -1 -db.Limit(10).Find(&users1).Limit(-1).Find(&users2) -//// SELECT * FROM users LIMIT 10; (users1) -//// SELECT * FROM users; (users2) -``` - -## Offset - -```go -db.Offset(3).Find(&users) -//// SELECT * FROM users OFFSET 3; - -// Cancel offset condition with -1 -db.Offset(10).Find(&users1).Offset(-1).Find(&users2) -//// SELECT * FROM users OFFSET 10; (users1) -//// SELECT * FROM users; (users2) -``` - -## Count - -```go -db.Where("name = ?", "jinzhu").Or("name = ?", "jinzhu 2").Find(&users).Count(&count) -//// SELECT * from USERS WHERE name = 'jinzhu' OR name = 'jinzhu 2'; (users) -//// SELECT count(*) FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'; (count) - -db.Model(&User{}).Where("name = ?", "jinzhu").Count(&count) -//// SELECT count(*) FROM users WHERE name = 'jinzhu'; (count) - -db.Table("deleted_users").Count(&count) -//// SELECT count(*) FROM deleted_users; -``` - -## Group & Having - -```go -rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Rows() -for rows.Next() { - ... -} - -rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Rows() -for rows.Next() { - ... -} - -type Result struct { - Date time.Time - Total int64 -} -db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Scan(&results) -``` - -## Joins - -```go -rows, err := db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Rows() -for rows.Next() { - ... -} - -db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results) - -// multiple joins with parameter -db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?", "411111111111").Find(&user) -``` - -## Transactions - -To perform a set of operations within a transaction, the general flow is as below. -The database handle returned from ``` db.Begin() ``` should be used for all operations within the transaction. -(Note that all individual save and delete operations are run in a transaction by default.) - -```go -// begin -tx := db.Begin() - -// do some database operations (use 'tx' from this point, not 'db') -tx.Create(...) -... - -// rollback in case of error -tx.Rollback() - -// Or commit if all is ok -tx.Commit() -``` - -### A Specific Example -``` -func CreateAnimals(db *gorm.DB) err { - tx := db.Begin() - // Note the use of tx as the database handle once you are within a transaction - - if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil { - tx.Rollback() - return err - } - - if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil { - tx.Rollback() - return err - } - - tx.Commit() - return nil -} -``` - -## Scopes - -```go -func AmountGreaterThan1000(db *gorm.DB) *gorm.DB { - return db.Where("amount > ?", 1000) -} - -func PaidWithCreditCard(db *gorm.DB) *gorm.DB { - return db.Where("pay_mode_sign = ?", "C") -} - -func PaidWithCod(db *gorm.DB) *gorm.DB { - return db.Where("pay_mode_sign = ?", "C") -} - -func OrderStatus(status []string) func (db *gorm.DB) *gorm.DB { - return func (db *gorm.DB) *gorm.DB { - return db.Scopes(AmountGreaterThan1000).Where("status in (?)", status) - } -} - -db.Scopes(AmountGreaterThan1000, PaidWithCreditCard).Find(&orders) -// Find all credit card orders and amount greater than 1000 - -db.Scopes(AmountGreaterThan1000, PaidWithCod).Find(&orders) -// Find all COD orders and amount greater than 1000 - -db.Scopes(OrderStatus([]string{"paid", "shipped"})).Find(&orders) -// Find all paid, shipped orders -``` - -## Callbacks - -Callbacks are methods defined on the pointer of struct. -If any callback returns an error, gorm will stop future operations and rollback all changes. - -Here is the list of all available callbacks: -(listed in the same order in which they will get called during the respective operations) - -### Creating An Object - -```go -BeforeSave -BeforeCreate -// save before associations -// save self -// save after associations -AfterCreate -AfterSave -``` -### Updating An Object - -```go -BeforeSave -BeforeUpdate -// save before associations -// save self -// save after associations -AfterUpdate -AfterSave -``` - -### Destroying An Object - -```go -BeforeDelete -// delete self -AfterDelete -``` - -### After Find - -```go -// load data from database -AfterFind -``` - -### Example - -```go -func (u *User) BeforeUpdate() (err error) { - if u.readonly() { - err = errors.New("read only user") - } - return -} - -// Rollback the insertion if user's id greater than 1000 -func (u *User) AfterCreate() (err error) { - if (u.Id > 1000) { - err = errors.New("user id is already greater than 1000") - } - return -} -``` - -Save/delete operations in gorm are running in a transaction. -Changes made in that transaction are not visible unless it is commited. -So if you want to use those changes in your callbacks, you need to run your SQL in the same transaction. -For this Gorm supports passing transactions to callbacks like this: - -```go -func (u *User) AfterCreate(tx *gorm.DB) (err error) { - tx.Model(u).Update("role", "admin") - return -} -``` - -## Pluck - -Get selected attributes as map - -```go -var ages []int64 -db.Find(&users).Pluck("age", &ages) - -var names []string -db.Model(&User{}).Pluck("name", &names) - -db.Table("deleted_users").Pluck("name", &names) - -// Requesting more than one column? Do it like this: -db.Select("name, age").Find(&users) -``` - -## Scan - -Scan results into another struct. - -```go -type Result struct { - Name string - Age int -} - -var result Result -db.Table("users").Select("name, age").Where("name = ?", 3).Scan(&result) - -// Raw SQL -db.Raw("SELECT name, age FROM users WHERE name = ?", 3).Scan(&result) -``` - -## Raw SQL - -```go -db.Exec("DROP TABLE users;") -db.Exec("UPDATE orders SET shipped_at=? WHERE id IN (?)", time.Now, []int64{11,22,33}) -``` - -## Row & Rows - -It is even possible to get query result as `*sql.Row` or `*sql.Rows` - -```go -row := db.Table("users").Where("name = ?", "jinzhu").Select("name, age").Row() // (*sql.Row) -row.Scan(&name, &age) - -rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error) -defer rows.Close() -for rows.Next() { - ... - rows.Scan(&name, &age, &email) - ... -} - -// Raw SQL -rows, err := db.Raw("select name, age, email from users where name = ?", "jinzhu").Rows() // (*sql.Rows, error) -defer rows.Close() -for rows.Next() { - ... - rows.Scan(&name, &age, &email) - ... -} -``` - -### Scan Rows - -```go -rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error) -defer rows.Close() - -for rows.Next() { - var user User - db.ScanRows(rows, &user) - // do something -} -``` - -## Specifying The Table Name - -```go -// Create `deleted_users` table with struct User's definition -db.Table("deleted_users").CreateTable(&User{}) - -var deleted_users []User -db.Table("deleted_users").Find(&deleted_users) -//// SELECT * FROM deleted_users; - -db.Table("deleted_users").Where("name = ?", "jinzhu").Delete() -//// DELETE FROM deleted_users WHERE name = 'jinzhu'; -``` - -### Specifying The Table Name For A Struct Permanently with TableName - -```go -type Cart struct { -} - -func (c Cart) TableName() string { - return "shopping_cart" -} - -func (u User) TableName() string { - if u.Role == "admin" { - return "admin_users" - } else { - return "users" - } -} -``` - -## Error Handling - -```go -query := db.Where("name = ?", "jinzhu").First(&user) -query := db.First(&user).Limit(10).Find(&users) -// query.Error will return the last happened error - -// So you could do error handing in your application like this: -if err := db.Where("name = ?", "jinzhu").First(&user).Error; err != nil { - // error handling... -} - -// RecordNotFound -// If no record found when you query data, gorm will return RecordNotFound error, you could check it like this: -db.Where("name = ?", "hello world").First(&User{}).Error == gorm.RecordNotFound -// Or use the shortcut method -db.Where("name = ?", "hello world").First(&user).RecordNotFound() - -if db.Model(&user).Related(&credit_card).RecordNotFound() { - // no credit card found error handling -} -``` - -## Logger - -Gorm has built-in logger support - -```go -// Enable Logger -db.LogMode(true) - -// Diable Logger -db.LogMode(false) - -// Debug a single operation -db.Debug().Where("name = ?", "jinzhu").First(&User{}) -``` - -![logger](https://raw.github.com/jinzhu/gorm/master/doc/logger.png) - -### Customize Logger - -```go -// Refer gorm's default logger for how to: https://github.com/jinzhu/gorm/blob/master/logger.go#files -db.SetLogger(gorm.Logger{revel.TRACE}) -db.SetLogger(log.New(os.Stdout, "\r\n", 0)) -``` - -## Existing Schema - -If you have an existing database schema, and the primary key field is different from `id`, you can add a tag to the field structure to specify that this field is a primary key. - -```go -type Animal struct { - AnimalId int64 `gorm:"primary_key"` - Birthday time.Time `sql:"DEFAULT:current_timestamp"` - Name string `sql:"default:'galeone'"` - Age int64 -} -``` - -If your column names differ from the struct fields, you can specify them like this: - -```go -type Animal struct { - AnimalId int64 `gorm:"column:beast_id;primary_key"` - Birthday time.Time `gorm:"column:day_of_the_beast"` - Age int64 `gorm:"column:age_of_the_beast"` -} -``` - -## Composite Primary Key - -```go -type Product struct { - ID string `gorm:"primary_key"` - LanguageCode string `gorm:"primary_key"` -} -``` - -## Database Indexes & Foreign Key - -```go -// Add foreign key -// 1st param : foreignkey field -// 2nd param : destination table(id) -// 3rd param : ONDELETE -// 4th param : ONUPDATE -db.Model(&User{}).AddForeignKey("city_id", "cities(id)", "RESTRICT", "RESTRICT") - -// Add index -db.Model(&User{}).AddIndex("idx_user_name", "name") - -// Multiple column index -db.Model(&User{}).AddIndex("idx_user_name_age", "name", "age") - -// Add unique index -db.Model(&User{}).AddUniqueIndex("idx_user_name", "name") - -// Multiple column unique index -db.Model(&User{}).AddUniqueIndex("idx_user_name_age", "name", "age") - -// Remove index -db.Model(&User{}).RemoveIndex("idx_user_name") -``` - -## More examples with query chain - -```go -db.First(&first_article).Count(&total_count).Limit(10).Find(&first_page_articles).Offset(10).Find(&second_page_articles) -//// SELECT * FROM articles LIMIT 1; (first_article) -//// SELECT count(*) FROM articles; (total_count) -//// SELECT * FROM articles LIMIT 10; (first_page_articles) -//// SELECT * FROM articles LIMIT 10 OFFSET 10; (second_page_articles) - - -db.Where("created_at > ?", "2013-10-10").Find(&cancelled_orders, "state = ?", "cancelled").Find(&shipped_orders, "state = ?", "shipped") -//// SELECT * FROM orders WHERE created_at > '2013/10/10' AND state = 'cancelled'; (cancelled_orders) -//// SELECT * FROM orders WHERE created_at > '2013/10/10' AND state = 'shipped'; (shipped_orders) - - -// Use variables to keep query chain -todays_orders := db.Where("created_at > ?", "2013-10-29") -cancelled_orders := todays_orders.Where("state = ?", "cancelled") -shipped_orders := todays_orders.Where("state = ?", "shipped") - - -// Search with shared conditions for different tables -db.Where("product_name = ?", "fancy_product").Find(&orders).Find(&shopping_carts) -//// SELECT * FROM orders WHERE product_name = 'fancy_product'; (orders) -//// SELECT * FROM carts WHERE product_name = 'fancy_product'; (shopping_carts) - - -// Search with shared conditions from different tables with specified table -db.Where("mail_type = ?", "TEXT").Find(&users1).Table("deleted_users").Find(&users2) -//// SELECT * FROM users WHERE mail_type = 'TEXT'; (users1) -//// SELECT * FROM deleted_users WHERE mail_type = 'TEXT'; (users2) - - -// FirstOrCreate example -db.Where("email = ?", "x@example.org").Attrs(User{RegisteredIp: "111.111.111.111"}).FirstOrCreate(&user) -//// SELECT * FROM users WHERE email = 'x@example.org'; -//// INSERT INTO "users" (email,registered_ip) VALUES ("x@example.org", "111.111.111.111") // if record not found -``` - -## Documentation - -[![GoDoc](https://godoc.org/github.com/jinzhu/gorm?status.svg)](https://godoc.org/github.com/jinzhu/gorm) - -`go doc` format documentation for this project can be viewed online without -installing the package by using the GoDoc page at: -http://godoc.org/github.com/jinzhu/gorm - -## TODO -* Github Pages - # Author **jinzhu** diff --git a/doc/README.md b/doc/README.md index a95b2959..143b44c1 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,4 +1,4 @@ -# GORM Guides +# GORM The fantastic ORM library for Golang, aims to be developer friendly. @@ -28,6 +28,35 @@ The fantastic ORM library for Golang, aims to be developer friendly. go get -u github.com/jinzhu/gorm ``` +## Basic Usage + +```go +type Product struct { + gorm.Model + Code string + Price uint +} + +var db *gorm.DB + +func init() { + var err error + db, err = gorm.Open("sqlite", "test.db") +} + +func main() { + db.Create(&Product{Code: "L1212", Price: 1000}) + + var product Product + db.First(&product, 1) // find product with id 1 + db.First(&product, "code = ?", "L1212") // find product with code l1212 + + db.Model(&product).Update("Price", 2000) // update product's price to 2000 + + db.Delete(&product) // delete product +} +``` + # Author **jinzhu** diff --git a/doc/advanced/advanced.md b/doc/advanced/advanced.md new file mode 100644 index 00000000..ec2c36cd --- /dev/null +++ b/doc/advanced/advanced.md @@ -0,0 +1,10 @@ +# Advanced Usage + +{% for section in book.chapters["advanced/advanced.md"].sections %} +* [**{{section.name}}**](../{{section.path}}) +{% if section["sections"] %}{% for subsection in section.sections %} + * [**{{ subsection.name }}**]({{ subsection.path }}) +{% endfor %}{% endif %} +{% endfor %} + + diff --git a/doc/advanced/transaction.md b/doc/advanced/transactions.md similarity index 99% rename from doc/advanced/transaction.md rename to doc/advanced/transactions.md index bef82561..d18cd722 100644 --- a/doc/advanced/transaction.md +++ b/doc/advanced/transactions.md @@ -20,7 +20,8 @@ tx.Commit() ``` ### A Specific Example -``` + +```go func CreateAnimals(db *gorm.DB) err { tx := db.Begin() // Note the use of tx as the database handle once you are within a transaction diff --git a/doc/book.json b/doc/book.json index 9e347864..86b9ca47 100644 --- a/doc/book.json +++ b/doc/book.json @@ -2,7 +2,7 @@ "title": "GORM Guide", "plugins": [ "prism", "-highlight", "collapsible-menu", "toc", - "github", "anchors", "edit-link" + "github", "edit-link" ], "pluginsConfig": { "toc": { diff --git a/doc/curd/create.md b/doc/curd/create.md index c450d902..614588f2 100644 --- a/doc/curd/create.md +++ b/doc/curd/create.md @@ -73,3 +73,4 @@ func (user *User) BeforeCreate(scope *gorm.Scope) error { // Add extra SQL option for inserting SQL db.Set("gorm:insert_option", "ON CONFLICT").Create(&product) // INSERT INTO products (name, code) VALUES ("name", "code") ON CONFLICT; +``` From 547210665de1c3bf0f12e6e850ddde6007115a5e Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Fri, 26 Feb 2016 21:53:28 +0800 Subject: [PATCH 65/83] Update Documents --- doc/SUMMARY.md | 38 ++- doc/advanced.md | 149 +++++++++++ doc/advanced/advanced.md | 10 - doc/advanced/compose-primary-key.md | 8 - doc/advanced/error-handling.md | 22 -- doc/advanced/logger.md | 24 -- doc/advanced/query-chain.md | 38 --- doc/advanced/sql-builder.md | 45 ---- doc/advanced/transactions.md | 42 --- doc/associations.md | 150 +++++++++++ doc/associations/association-mode.md | 40 --- doc/associations/associations.md | 8 - doc/associations/belongs-to.md | 18 -- doc/associations/has-many.md | 18 -- doc/associations/has-one.md | 22 -- doc/associations/many-to-many.md | 17 -- doc/associations/polymorphism.md | 26 -- doc/book.json | 61 +---- doc/{callbacks => }/callbacks.md | 2 + doc/{curd/query.md => curd.md} | 298 +++++++++++++++++++-- doc/curd/create.md | 76 ------ doc/curd/curd.md | 9 - doc/curd/delete.md | 44 --- doc/curd/preloading.md | 40 --- doc/curd/update.md | 120 --------- doc/{database/migration.md => database.md} | 67 +++++ doc/database/connect-database.md | 66 ----- doc/database/database.md | 5 - doc/{development => }/development.md | 2 + doc/{models/defination.md => models.md} | 0 doc/models/models.md | 8 - doc/models/naming-conventions.md | 0 32 files changed, 673 insertions(+), 800 deletions(-) create mode 100644 doc/advanced.md delete mode 100644 doc/advanced/advanced.md delete mode 100644 doc/advanced/compose-primary-key.md delete mode 100644 doc/advanced/error-handling.md delete mode 100644 doc/advanced/logger.md delete mode 100644 doc/advanced/query-chain.md delete mode 100644 doc/advanced/sql-builder.md delete mode 100644 doc/advanced/transactions.md create mode 100644 doc/associations.md delete mode 100644 doc/associations/association-mode.md delete mode 100644 doc/associations/associations.md delete mode 100644 doc/associations/belongs-to.md delete mode 100644 doc/associations/has-many.md delete mode 100644 doc/associations/has-one.md delete mode 100644 doc/associations/many-to-many.md delete mode 100644 doc/associations/polymorphism.md rename doc/{callbacks => }/callbacks.md (99%) rename doc/{curd/query.md => curd.md} (55%) delete mode 100644 doc/curd/create.md delete mode 100644 doc/curd/curd.md delete mode 100644 doc/curd/delete.md delete mode 100644 doc/curd/preloading.md delete mode 100644 doc/curd/update.md rename doc/{database/migration.md => database.md} (59%) delete mode 100644 doc/database/connect-database.md delete mode 100644 doc/database/database.md rename doc/{development => }/development.md (99%) rename doc/{models/defination.md => models.md} (100%) delete mode 100644 doc/models/models.md delete mode 100644 doc/models/naming-conventions.md diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md index fee9b9e7..dc76398f 100644 --- a/doc/SUMMARY.md +++ b/doc/SUMMARY.md @@ -1,13 +1,31 @@ # Summary -* [GORM Guides](http://github.com/jinzhu/gorm) - * [Getting Started with GORM](README.md) -{% for path, chapter in book.chapters %} -* [{{ chapter.name }}]({{ path }}) - {% for section in chapter.sections %} - * [{{ section.name }}]({{ section.path }}) - {% if section["sections"] %}{% for subsection in section.sections %} - * [{{ subsection.name }}]({{ subsection.path }}) - {% endfor %}{% endif %}{% endfor %} -{% endfor %} +* [Database](database.md) + * [Database Connection](database.md#connecting-to-a-database) + * [Migration](database.md#migration) +* [Models](models.md) + * [Model Defination](models.md#model-defination), + * [Naming Conventions & Overriding](models.md#conventions-overriding-conventions), + * [Associations](associations.md) + * [Belongs To](associations.md#belongs-to) + * [Has One](associations.md#has-one) + * [Has Many](associations.md#has-many) + * [Many To Many](associations.md#many-to-many) + * [Polymorphism](associations.md#polymorphism) + * [Association Mode](associations.md#association-mode) +* [CRUD: Reading and Writing Data](curd.md) + * [Create](curd.md#create), + * [Query](curd.md#query), + * [Preloading (Eager Loading)](curd.md#preloading), + * [Update](curd.md#update), + * [Delete / Soft Delete](curd.md#delete) +* [Callbacks](callbacks.md) +* [Advanced Usage](advanced.md) + * [Error Handling](advanced.md#error-handling) + * [Transactions](advanced.md#transactions) + * [Raw SQL & SQL Builder](advanced.md#sql-builder) + * [Composite Primary Key](advanced.md#compose-primary-key) + * [Overriding Logger](advanced.md#logger) +* [Development](development.md) + * [Write Plugins](development.md#callbacks) diff --git a/doc/advanced.md b/doc/advanced.md new file mode 100644 index 00000000..0d8c5827 --- /dev/null +++ b/doc/advanced.md @@ -0,0 +1,149 @@ +# Advanced Usage + + + +## Error Handling + +```go +query := db.Where("name = ?", "jinzhu").First(&user) +query := db.First(&user).Limit(10).Find(&users) +// query.Error will return the last happened error + +// So you could do error handing in your application like this: +if err := db.Where("name = ?", "jinzhu").First(&user).Error; err != nil { + // error handling... +} + +// RecordNotFound +// If no record found when you query data, gorm will return RecordNotFound error, you could check it like this: +db.Where("name = ?", "hello world").First(&User{}).Error == gorm.RecordNotFound +// Or use the shortcut method +db.Where("name = ?", "hello world").First(&user).RecordNotFound() + +if db.Model(&user).Related(&credit_card).RecordNotFound() { + // no credit card found error handling +} +``` + +## Transactions + +To perform a set of operations within a transaction, the general flow is as below. +The database handle returned from ``` db.Begin() ``` should be used for all operations within the transaction. +(Note that all individual save and delete operations are run in a transaction by default.) + +```go +// begin +tx := db.Begin() + +// do some database operations (use 'tx' from this point, not 'db') +tx.Create(...) +... + +// rollback in case of error +tx.Rollback() + +// Or commit if all is ok +tx.Commit() +``` + +### A Specific Example + +```go +func CreateAnimals(db *gorm.DB) err { + tx := db.Begin() + // Note the use of tx as the database handle once you are within a transaction + + if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil { + tx.Rollback() + return err + } + + if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil { + tx.Rollback() + return err + } + + tx.Commit() + return nil +} +``` + +## Raw SQL + +```go +db.Exec("DROP TABLE users;") +db.Exec("UPDATE orders SET shipped_at=? WHERE id IN (?)", time.Now, []int64{11,22,33}) +``` + +## Row & Rows + +It is even possible to get query result as `*sql.Row` or `*sql.Rows` + +```go +row := db.Table("users").Where("name = ?", "jinzhu").Select("name, age").Row() // (*sql.Row) +row.Scan(&name, &age) + +rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error) +defer rows.Close() +for rows.Next() { + ... + rows.Scan(&name, &age, &email) + ... +} + +// Raw SQL +rows, err := db.Raw("select name, age, email from users where name = ?", "jinzhu").Rows() // (*sql.Rows, error) +defer rows.Close() +for rows.Next() { + ... + rows.Scan(&name, &age, &email) + ... +} +``` + +### Scan Rows + +```go +rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error) +defer rows.Close() + +for rows.Next() { + var user User + db.ScanRows(rows, &user) + // do something +} +``` + +## Composite Primary Key + +```go +type Product struct { + ID string `gorm:"primary_key"` + LanguageCode string `gorm:"primary_key"` +} +``` + +## Logger + +Gorm has built-in logger support + +```go +// Enable Logger +db.LogMode(true) + +// Diable Logger +db.LogMode(false) + +// Debug a single operation +db.Debug().Where("name = ?", "jinzhu").First(&User{}) +``` + +![logger](https://raw.github.com/jinzhu/gorm/master/doc/logger.png) + +### Customize Logger + +```go +// Refer gorm's default logger for how to: https://github.com/jinzhu/gorm/blob/master/logger.go#files +db.SetLogger(gorm.Logger{revel.TRACE}) +db.SetLogger(log.New(os.Stdout, "\r\n", 0)) +``` diff --git a/doc/advanced/advanced.md b/doc/advanced/advanced.md deleted file mode 100644 index ec2c36cd..00000000 --- a/doc/advanced/advanced.md +++ /dev/null @@ -1,10 +0,0 @@ -# Advanced Usage - -{% for section in book.chapters["advanced/advanced.md"].sections %} -* [**{{section.name}}**](../{{section.path}}) -{% if section["sections"] %}{% for subsection in section.sections %} - * [**{{ subsection.name }}**]({{ subsection.path }}) -{% endfor %}{% endif %} -{% endfor %} - - diff --git a/doc/advanced/compose-primary-key.md b/doc/advanced/compose-primary-key.md deleted file mode 100644 index b8bae65e..00000000 --- a/doc/advanced/compose-primary-key.md +++ /dev/null @@ -1,8 +0,0 @@ -# Composite Primary Key - -```go -type Product struct { - ID string `gorm:"primary_key"` - LanguageCode string `gorm:"primary_key"` -} -``` diff --git a/doc/advanced/error-handling.md b/doc/advanced/error-handling.md deleted file mode 100644 index 7215498f..00000000 --- a/doc/advanced/error-handling.md +++ /dev/null @@ -1,22 +0,0 @@ -# Error Handling - -```go -query := db.Where("name = ?", "jinzhu").First(&user) -query := db.First(&user).Limit(10).Find(&users) -// query.Error will return the last happened error - -// So you could do error handing in your application like this: -if err := db.Where("name = ?", "jinzhu").First(&user).Error; err != nil { - // error handling... -} - -// RecordNotFound -// If no record found when you query data, gorm will return RecordNotFound error, you could check it like this: -db.Where("name = ?", "hello world").First(&User{}).Error == gorm.RecordNotFound -// Or use the shortcut method -db.Where("name = ?", "hello world").First(&user).RecordNotFound() - -if db.Model(&user).Related(&credit_card).RecordNotFound() { - // no credit card found error handling -} -``` diff --git a/doc/advanced/logger.md b/doc/advanced/logger.md deleted file mode 100644 index b45300d1..00000000 --- a/doc/advanced/logger.md +++ /dev/null @@ -1,24 +0,0 @@ -# Logger - -Gorm has built-in logger support - -```go -// Enable Logger -db.LogMode(true) - -// Diable Logger -db.LogMode(false) - -// Debug a single operation -db.Debug().Where("name = ?", "jinzhu").First(&User{}) -``` - -![logger](https://raw.github.com/jinzhu/gorm/master/doc/logger.png) - -### Customize Logger - -```go -// Refer gorm's default logger for how to: https://github.com/jinzhu/gorm/blob/master/logger.go#files -db.SetLogger(gorm.Logger{revel.TRACE}) -db.SetLogger(log.New(os.Stdout, "\r\n", 0)) -``` diff --git a/doc/advanced/query-chain.md b/doc/advanced/query-chain.md deleted file mode 100644 index c3691a91..00000000 --- a/doc/advanced/query-chain.md +++ /dev/null @@ -1,38 +0,0 @@ -# Query Chain - -```go -db.First(&first_article).Count(&total_count).Limit(10).Find(&first_page_articles).Offset(10).Find(&second_page_articles) -//// SELECT * FROM articles LIMIT 1; (first_article) -//// SELECT count(*) FROM articles; (total_count) -//// SELECT * FROM articles LIMIT 10; (first_page_articles) -//// SELECT * FROM articles LIMIT 10 OFFSET 10; (second_page_articles) - - -db.Where("created_at > ?", "2013-10-10").Find(&cancelled_orders, "state = ?", "cancelled").Find(&shipped_orders, "state = ?", "shipped") -//// SELECT * FROM orders WHERE created_at > '2013/10/10' AND state = 'cancelled'; (cancelled_orders) -//// SELECT * FROM orders WHERE created_at > '2013/10/10' AND state = 'shipped'; (shipped_orders) - - -// Use variables to keep query chain -todays_orders := db.Where("created_at > ?", "2013-10-29") -cancelled_orders := todays_orders.Where("state = ?", "cancelled") -shipped_orders := todays_orders.Where("state = ?", "shipped") - - -// Search with shared conditions for different tables -db.Where("product_name = ?", "fancy_product").Find(&orders).Find(&shopping_carts) -//// SELECT * FROM orders WHERE product_name = 'fancy_product'; (orders) -//// SELECT * FROM carts WHERE product_name = 'fancy_product'; (shopping_carts) - - -// Search with shared conditions from different tables with specified table -db.Where("mail_type = ?", "TEXT").Find(&users1).Table("deleted_users").Find(&users2) -//// SELECT * FROM users WHERE mail_type = 'TEXT'; (users1) -//// SELECT * FROM deleted_users WHERE mail_type = 'TEXT'; (users2) - - -// FirstOrCreate example -db.Where("email = ?", "x@example.org").Attrs(User{RegisteredIp: "111.111.111.111"}).FirstOrCreate(&user) -//// SELECT * FROM users WHERE email = 'x@example.org'; -//// INSERT INTO "users" (email,registered_ip) VALUES ("x@example.org", "111.111.111.111") // if record not found -``` diff --git a/doc/advanced/sql-builder.md b/doc/advanced/sql-builder.md deleted file mode 100644 index 832b6cfc..00000000 --- a/doc/advanced/sql-builder.md +++ /dev/null @@ -1,45 +0,0 @@ -## Raw SQL - -```go -db.Exec("DROP TABLE users;") -db.Exec("UPDATE orders SET shipped_at=? WHERE id IN (?)", time.Now, []int64{11,22,33}) -``` - -## Row & Rows - -It is even possible to get query result as `*sql.Row` or `*sql.Rows` - -```go -row := db.Table("users").Where("name = ?", "jinzhu").Select("name, age").Row() // (*sql.Row) -row.Scan(&name, &age) - -rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error) -defer rows.Close() -for rows.Next() { - ... - rows.Scan(&name, &age, &email) - ... -} - -// Raw SQL -rows, err := db.Raw("select name, age, email from users where name = ?", "jinzhu").Rows() // (*sql.Rows, error) -defer rows.Close() -for rows.Next() { - ... - rows.Scan(&name, &age, &email) - ... -} -``` - -### Scan Rows - -```go -rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error) -defer rows.Close() - -for rows.Next() { - var user User - db.ScanRows(rows, &user) - // do something -} -``` diff --git a/doc/advanced/transactions.md b/doc/advanced/transactions.md deleted file mode 100644 index d18cd722..00000000 --- a/doc/advanced/transactions.md +++ /dev/null @@ -1,42 +0,0 @@ -## Transactions - -To perform a set of operations within a transaction, the general flow is as below. -The database handle returned from ``` db.Begin() ``` should be used for all operations within the transaction. -(Note that all individual save and delete operations are run in a transaction by default.) - -```go -// begin -tx := db.Begin() - -// do some database operations (use 'tx' from this point, not 'db') -tx.Create(...) -... - -// rollback in case of error -tx.Rollback() - -// Or commit if all is ok -tx.Commit() -``` - -### A Specific Example - -```go -func CreateAnimals(db *gorm.DB) err { - tx := db.Begin() - // Note the use of tx as the database handle once you are within a transaction - - if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil { - tx.Rollback() - return err - } - - if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil { - tx.Rollback() - return err - } - - tx.Commit() - return nil -} -``` diff --git a/doc/associations.md b/doc/associations.md new file mode 100644 index 00000000..74e684c9 --- /dev/null +++ b/doc/associations.md @@ -0,0 +1,150 @@ +# Associations + + + +## Belongs To + +```go +// User belongs to a profile, ProfileID is the foreign key +type User struct { + gorm.Model + Profile Profile + ProfileID int +} + +type Profile struct { + gorm.Model + Name string +} + +db.Model(&user).Related(&profile) +//// SELECT * FROM profiles WHERE id = 111; // 111 is user's foreign key ProfileID +``` + +## Has One + +```go +// User has one CreditCard, UserID is the foreign key +type User struct { + gorm.Model + CreditCard CreditCard +} + +type CreditCard struct { + gorm.Model + UserID uint + Number string +} + +var card CreditCard +db.Model(&user).Related(&card, "CreditCard") +//// SELECT * FROM credit_cards WHERE user_id = 123; // 123 is user's primary key +// CreditCard is user's field name, it means get user's CreditCard relations and fill it into variable card +// If the field name is same as the variable's type name, like above example, it could be omitted, like: +db.Model(&user).Related(&card) +``` + +## Has Many + +```go +// User has many emails, UserID is the foreign key +type User struct { + gorm.Model + Emails []Email +} + +type Email struct { + gorm.Model + Email string + UserID uint +} + +db.Model(&user).Related(&emails) +//// SELECT * FROM emails WHERE user_id = 111; // 111 is user's primary key +``` + +## Many To Many + +```go +// User has and belongs to many languages, use `user_languages` as join table +type User struct { + gorm.Model + Languages []Language `gorm:"many2many:user_languages;"` +} + +type Language struct { + gorm.Model + Name string +} + +db.Model(&user).Related(&languages) +//// SELECT * FROM "languages" INNER JOIN "user_languages" ON "user_languages"."language_id" = "languages"."id" WHERE "user_languages"."user_id" = 111 +``` + +## Polymorphism + +Supports polymorphic has-many and has-one associations. + +```go + type Cat struct { + Id int + Name string + Toy Toy `gorm:"polymorphic:Owner;"` + } + + type Dog struct { + Id int + Name string + Toy Toy `gorm:"polymorphic:Owner;"` + } + + type Toy struct { + Id int + Name string + OwnerId int + OwnerType string + } +``` + +Note: polymorphic belongs-to and many-to-many are explicitly NOT supported, and will throw errors. + +## Association Mode + +Association Mode contains some helper methods to handle relationship things easily. + +```go +// Start Association Mode +var user User +db.Model(&user).Association("Languages") +// `user` is the source, it need to be a valid record (contains primary key) +// `Languages` is source's field name for a relationship. +// If those conditions not matched, will return an error, check it with: +// db.Model(&user).Association("Languages").Error + + +// Query - Find out all related associations +db.Model(&user).Association("Languages").Find(&languages) + + +// Append - Append new associations for many2many, has_many, will replace current association for has_one, belongs_to +db.Model(&user).Association("Languages").Append([]Language{languageZH, languageEN}) +db.Model(&user).Association("Languages").Append(Language{Name: "DE"}) + + +// Delete - Remove relationship between source & passed arguments, won't delete those arguments +db.Model(&user).Association("Languages").Delete([]Language{languageZH, languageEN}) +db.Model(&user).Association("Languages").Delete(languageZH, languageEN) + + +// Replace - Replace current associations with new one +db.Model(&user).Association("Languages").Replace([]Language{languageZH, languageEN}) +db.Model(&user).Association("Languages").Replace(Language{Name: "DE"}, languageEN) + + +// Count - Return the count of current associations +db.Model(&user).Association("Languages").Count() + + +// Clear - Remove relationship between source & current associations, won't delete those associations +db.Model(&user).Association("Languages").Clear() +``` diff --git a/doc/associations/association-mode.md b/doc/associations/association-mode.md deleted file mode 100644 index ca15c962..00000000 --- a/doc/associations/association-mode.md +++ /dev/null @@ -1,40 +0,0 @@ -# Association Mode - -Association Mode contains some helper methods to handle relationship things easily. - -```go -// Start Association Mode -var user User -db.Model(&user).Association("Languages") -// `user` is the source, it need to be a valid record (contains primary key) -// `Languages` is source's field name for a relationship. -// If those conditions not matched, will return an error, check it with: -// db.Model(&user).Association("Languages").Error - - -// Query - Find out all related associations -db.Model(&user).Association("Languages").Find(&languages) - - -// Append - Append new associations for many2many, has_many, will replace current association for has_one, belongs_to -db.Model(&user).Association("Languages").Append([]Language{languageZH, languageEN}) -db.Model(&user).Association("Languages").Append(Language{Name: "DE"}) - - -// Delete - Remove relationship between source & passed arguments, won't delete those arguments -db.Model(&user).Association("Languages").Delete([]Language{languageZH, languageEN}) -db.Model(&user).Association("Languages").Delete(languageZH, languageEN) - - -// Replace - Replace current associations with new one -db.Model(&user).Association("Languages").Replace([]Language{languageZH, languageEN}) -db.Model(&user).Association("Languages").Replace(Language{Name: "DE"}, languageEN) - - -// Count - Return the count of current associations -db.Model(&user).Association("Languages").Count() - - -// Clear - Remove relationship between source & current associations, won't delete those associations -db.Model(&user).Association("Languages").Clear() -``` diff --git a/doc/associations/associations.md b/doc/associations/associations.md deleted file mode 100644 index b0fc3ec4..00000000 --- a/doc/associations/associations.md +++ /dev/null @@ -1,8 +0,0 @@ -# Associations - -{% for section in book.chapters["associations/associations.md"].sections %} -* [**{{section.name}}**](../{{section.path}}) -{% if section["sections"] %}{% for subsection in section.sections %} - * [**{{ subsection.name }}**]({{ subsection.path }}) -{% endfor %}{% endif %} -{% endfor %} diff --git a/doc/associations/belongs-to.md b/doc/associations/belongs-to.md deleted file mode 100644 index d7ae8667..00000000 --- a/doc/associations/belongs-to.md +++ /dev/null @@ -1,18 +0,0 @@ -# Belongs To - -```go -// User belongs to a profile, ProfileID is the foreign key -type User struct { - gorm.Model - Profile Profile - ProfileID int -} - -type Profile struct { - gorm.Model - Name string -} - -db.Model(&user).Related(&profile) -//// SELECT * FROM profiles WHERE id = 111; // 111 is user's foreign key ProfileID -``` diff --git a/doc/associations/has-many.md b/doc/associations/has-many.md deleted file mode 100644 index 5cf15a47..00000000 --- a/doc/associations/has-many.md +++ /dev/null @@ -1,18 +0,0 @@ -# Has Many - -```go -// User has many emails, UserID is the foreign key -type User struct { - gorm.Model - Emails []Email -} - -type Email struct { - gorm.Model - Email string - UserID uint -} - -db.Model(&user).Related(&emails) -//// SELECT * FROM emails WHERE user_id = 111; // 111 is user's primary key -``` diff --git a/doc/associations/has-one.md b/doc/associations/has-one.md deleted file mode 100644 index 31bbacad..00000000 --- a/doc/associations/has-one.md +++ /dev/null @@ -1,22 +0,0 @@ -# Has One - -```go -// User has one CreditCard, UserID is the foreign key -type User struct { - gorm.Model - CreditCard CreditCard -} - -type CreditCard struct { - gorm.Model - UserID uint - Number string -} - -var card CreditCard -db.Model(&user).Related(&card, "CreditCard") -//// SELECT * FROM credit_cards WHERE user_id = 123; // 123 is user's primary key -// CreditCard is user's field name, it means get user's CreditCard relations and fill it into variable card -// If the field name is same as the variable's type name, like above example, it could be omitted, like: -db.Model(&user).Related(&card) -``` diff --git a/doc/associations/many-to-many.md b/doc/associations/many-to-many.md deleted file mode 100644 index af11dd35..00000000 --- a/doc/associations/many-to-many.md +++ /dev/null @@ -1,17 +0,0 @@ -# Many To Many - -```go -// User has and belongs to many languages, use `user_languages` as join table -type User struct { - gorm.Model - Languages []Language `gorm:"many2many:user_languages;"` -} - -type Language struct { - gorm.Model - Name string -} - -db.Model(&user).Related(&languages) -//// SELECT * FROM "languages" INNER JOIN "user_languages" ON "user_languages"."language_id" = "languages"."id" WHERE "user_languages"."user_id" = 111 -``` diff --git a/doc/associations/polymorphism.md b/doc/associations/polymorphism.md deleted file mode 100644 index 10e2307d..00000000 --- a/doc/associations/polymorphism.md +++ /dev/null @@ -1,26 +0,0 @@ -# Polymorphism - -Supports polymorphic has-many and has-one associations. - -```go - type Cat struct { - Id int - Name string - Toy Toy `gorm:"polymorphic:Owner;"` - } - - type Dog struct { - Id int - Name string - Toy Toy `gorm:"polymorphic:Owner;"` - } - - type Toy struct { - Id int - Name string - OwnerId int - OwnerType string - } -``` - -Note: polymorphic belongs-to and many-to-many are explicitly NOT supported, and will throw errors. diff --git a/doc/book.json b/doc/book.json index 86b9ca47..138e9782 100644 --- a/doc/book.json +++ b/doc/book.json @@ -1,7 +1,7 @@ { "title": "GORM Guide", "plugins": [ - "prism", "-highlight", "collapsible-menu", "toc", + "prism", "-highlight", "toc", "github", "edit-link" ], "pluginsConfig": { @@ -22,62 +22,5 @@ "content": "

Hello

", "type": "normal" } - ], - "variables": { - "chapters": { - "database/database.md": { - "name": "Databae", - "sections": [ - {"name": "Database Connection", "path": "database/connect-database.md"}, - {"name": "Migration", "path": "database/migration.md"} - ] - }, - "models/models.md": { - "name": "Models", - "sections": [ - {"name": "Model Defination", "path": "models/defination.md"}, - {"name": "Naming Conventions & Overriding", "path": "models/naming-conventions.md"}, - {"name": "Associations", "path": "associations/associations.md", "sections": - [ - {"name": "Belongs To", "path": "associations/belongs-to.md"}, - {"name": "Has One", "path": "associations/has-one.md"}, - {"name": "Has Many", "path": "associations/has-many.md"}, - {"name": "Many To Many", "path": "associations/many-to-many.md"}, - {"name": "Polymorphism", "path": "associations/polymorphism.md"}, - {"name": "Association Mode", "path": "associations/association-mode.md"} - ] - } - ] - }, - "curd/curd.md": { - "name": "CRUD: Reading and Writing Data", - "sections": [ - {"name": "Create", "path": "curd/create.md"}, - {"name": "Query", "path": "curd/query.md"}, - {"name": "Preloading (Eager Loading)", "path": "curd/preloading.md"}, - {"name": "Update", "path": "curd/update.md"}, - {"name": "Delete / Soft Delete", "path": "curd/delete.md"} - ] - }, - "callbacks/callbacks.md": { - "name": "Callbacks" - }, - "advanced/advanced.md": { - "name": "Advanced Usage", - "sections": [ - {"name": "Error Handling", "path": "advanced/error-handling.md"}, - {"name": "Transactions", "path": "advanced/transactions.md"}, - {"name": "Raw SQL & SQL Builder", "path": "advanced/sql-builder.md"}, - {"name": "Composite Primary Key", "path": "advanced/compose-primary-key.md"}, - {"name": "Overriding Logger", "path": "advanced/logger.md"} - ] - }, - "development/development.md": { - "name": "Development", - "sections": [ - {"name": "Write Plugins", "path": "development/plugins.md"} - ] - } - } - } + ] } diff --git a/doc/callbacks/callbacks.md b/doc/callbacks.md similarity index 99% rename from doc/callbacks/callbacks.md rename to doc/callbacks.md index 56c29193..18ef4cca 100644 --- a/doc/callbacks/callbacks.md +++ b/doc/callbacks.md @@ -1,5 +1,7 @@ # Callbacks + + Callbacks are methods defined on the pointer of struct. If any callback returns an error, gorm will stop future operations and rollback all changes. diff --git a/doc/curd/query.md b/doc/curd.md similarity index 55% rename from doc/curd/query.md rename to doc/curd.md index 0795c875..998c1fb9 100644 --- a/doc/curd/query.md +++ b/doc/curd.md @@ -1,4 +1,86 @@ -# Query +# CRUD: Reading and Writing Data + + + +## Create + +### Create Record + +```go +user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()} + +db.NewRecord(user) // => returns `true` as primary key is blank + +db.Create(&user) + +db.NewRecord(user) // => return `false` after `user` created + +// Associations will be inserted automatically when save the record +user := User{ + Name: "jinzhu", + BillingAddress: Address{Address1: "Billing Address - Address 1"}, + ShippingAddress: Address{Address1: "Shipping Address - Address 1"}, + Emails: []Email{{Email: "jinzhu@example.com"}, {Email: "jinzhu-2@example@example.com"}}, + Languages: []Language{{Name: "ZH"}, {Name: "EN"}}, +} + +db.Create(&user) +//// BEGIN TRANSACTION; +//// INSERT INTO "addresses" (address1) VALUES ("Billing Address - Address 1"); +//// INSERT INTO "addresses" (address1) VALUES ("Shipping Address - Address 1"); +//// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2); +//// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com"); +//// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu-2@example.com"); +//// INSERT INTO "languages" ("name") VALUES ('ZH'); +//// INSERT INTO user_languages ("user_id","language_id") VALUES (111, 1); +//// INSERT INTO "languages" ("name") VALUES ('EN'); +//// INSERT INTO user_languages ("user_id","language_id") VALUES (111, 2); +//// COMMIT; +``` + +### Create With Associations + +Refer Associations for more details + +### Default Values + +You could defined default value in the `sql` tag, then the generated creating SQL will ignore these fields that including default value and its value is blank, and after inserted the record into databae, gorm will load those fields's value from database. + +```go +type Animal struct { + ID int64 + Name string `sql:"default:'galeone'"` + Age int64 +} + +var animal = Animal{Age: 99, Name: ""} +db.Create(&animal) +// INSERT INTO animals("age") values('99'); +// SELECT name from animals WHERE ID=111; // the returning primary key is 111 +// animal.Name => 'galeone' +``` + +### Setting Primary Key In Callbacks + +If you want to set primary key in `BeforeCreate` callback, you could use `scope.SetColumn`, for example: + +```go +func (user *User) BeforeCreate(scope *gorm.Scope) error { + scope.SetColumn("ID", uuid.New()) + return nil +} +``` + +### Extra Creating option + +```go +// Add extra SQL option for inserting SQL +db.Set("gorm:insert_option", "ON CONFLICT").Create(&product) +// INSERT INTO products (name, code) VALUES ("name", "code") ON CONFLICT; +``` + + +## Query ```go // Get the first record @@ -18,7 +100,7 @@ db.First(&user, 10) //// SELECT * FROM users WHERE id = 10; ``` -## Query With Where (Plain SQL) +### Query With Where (Plain SQL) ```go // Get the first matched record @@ -46,7 +128,7 @@ db.Where("updated_at > ?", lastWeek).Find(&users) db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users) ``` -## Query With Where (Struct & Map) +### Query With Where (Struct & Map) ```go // Struct @@ -62,7 +144,7 @@ db.Where([]int64{20, 21, 22}).Find(&users) //// SELECT * FROM users WHERE id IN (20, 21, 22); ``` -## Query With Not +### Query With Not ```go db.Not("name", "jinzhu").First(&user) @@ -88,7 +170,7 @@ db.Not(User{Name: "jinzhu"}).First(&user) //// SELECT * FROM users WHERE name <> "jinzhu"; ``` -## Query With Inline Condition +### Query With Inline Condition ```go // Get by primary key @@ -111,7 +193,7 @@ db.Find(&users, map[string]interface{}{"age": 20}) //// SELECT * FROM users WHERE age = 20; ``` -## Query With Or +### Query With Or ```go db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users) @@ -125,7 +207,7 @@ db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2"}).Find(&users) db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2"}).Find(&users) ``` -## Query Chains +### Query Chains Gorm has a chainable API, you could use it like this @@ -136,7 +218,7 @@ db.Where("name <> ?","jinzhu").Where("age >= ? and role <> ?",20,"admin").Find(& db.Where("role = ?", "admin").Or("role = ?", "super_admin").Not("name = ?", "jinzhu").Find(&users) ``` -## Extra Querying option +### Extra Querying option ```go // Add extra SQL option for selecting SQL @@ -144,7 +226,7 @@ db.Set("gorm:query_option", "FOR UPDATE").First(&user, 10) //// SELECT * FROM users WHERE id = 10 FOR UPDATE; ``` -## FirstOrInit +### FirstOrInit Get the first matched record, or initialize a record with search conditions. @@ -160,7 +242,7 @@ db.FirstOrInit(&user, map[string]interface{}{"name": "jinzhu"}) //// user -> User{Id: 111, Name: "Jinzhu", Age: 20} ``` -### Attrs +#### Attrs Ignore some values when searching, but use them to initialize the struct if record is not found. @@ -180,7 +262,7 @@ db.Where(User{Name: "Jinzhu"}).Attrs(User{Age: 30}).FirstOrInit(&user) //// user -> User{Id: 111, Name: "Jinzhu", Age: 20} ``` -### Assign +#### Assign Ignore some values when searching, but assign it to the result regardless it is found or not. @@ -195,7 +277,7 @@ db.Where(User{Name: "Jinzhu"}).Assign(User{Age: 30}).FirstOrInit(&user) //// user -> User{Id: 111, Name: "Jinzhu", Age: 30} ``` -## FirstOrCreate +### FirstOrCreate Get the first matched record, or create with search conditions. @@ -210,7 +292,7 @@ db.Where(User{Name: "Jinzhu"}).FirstOrCreate(&user) //// user -> User{Id: 111, Name: "Jinzhu"} ``` -### Attrs +#### Attrs Ignore some values when searching, but use them to create the struct if record is not found. like `FirstOrInit` @@ -227,7 +309,7 @@ db.Where(User{Name: "jinzhu"}).Attrs(User{Age: 30}).FirstOrCreate(&user) //// user -> User{Id: 111, Name: "jinzhu", Age: 20} ``` -### Assign +#### Assign Ignore some values when searching, but assign it to the record regardless it is found or not, then save back to database. like `FirstOrInit` @@ -245,7 +327,7 @@ db.Where(User{Name: "jinzhu"}).Assign(User{Age: 30}).FirstOrCreate(&user) //// user -> User{Id: 111, Name: "jinzhu", Age: 30} ``` -## Select +### Select ```go db.Select("name, age").Find(&users) @@ -258,7 +340,7 @@ db.Table("users").Select("COALESCE(age,?)", 42).Rows() //// SELECT COALESCE(age,'42') FROM users; ``` -## Order +### Order ```go db.Order("age desc, name").Find(&users) @@ -274,7 +356,7 @@ db.Order("age desc").Find(&users1).Order("age", true).Find(&users2) //// SELECT * FROM users ORDER BY age; (users2) ``` -## Limit +### Limit ```go db.Limit(3).Find(&users) @@ -286,7 +368,7 @@ db.Limit(10).Find(&users1).Limit(-1).Find(&users2) //// SELECT * FROM users; (users2) ``` -## Offset +### Offset ```go db.Offset(3).Find(&users) @@ -298,7 +380,7 @@ db.Offset(10).Find(&users1).Offset(-1).Find(&users2) //// SELECT * FROM users; (users2) ``` -## Count +### Count ```go db.Where("name = ?", "jinzhu").Or("name = ?", "jinzhu 2").Find(&users).Count(&count) @@ -312,7 +394,7 @@ db.Table("deleted_users").Count(&count) //// SELECT count(*) FROM deleted_users; ``` -## Group & Having +### Group & Having ```go rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Rows() @@ -332,7 +414,7 @@ type Result struct { db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Scan(&results) ``` -## Joins +### Joins ```go rows, err := db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Rows() @@ -346,7 +428,7 @@ db.Table("users").Select("users.name, emails.email").Joins("left join emails on db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?", "411111111111").Find(&user) ``` -## Pluck +### Pluck Get selected attributes as map @@ -363,7 +445,7 @@ db.Table("deleted_users").Pluck("name", &names) db.Select("name, age").Find(&users) ``` -## Scan +### Scan Scan results into another struct. @@ -380,7 +462,7 @@ db.Table("users").Select("name, age").Where("name = ?", 3).Scan(&result) db.Raw("SELECT name, age FROM users WHERE name = ?", 3).Scan(&result) ``` -## Scopes +### Scopes ```go func AmountGreaterThan1000(db *gorm.DB) *gorm.DB { @@ -411,7 +493,7 @@ db.Scopes(OrderStatus([]string{"paid", "shipped"})).Find(&orders) // Find all paid, shipped orders ``` -## Specifying The Table Name +### Specifying The Table Name ```go // Create `deleted_users` table with struct User's definition @@ -424,3 +506,169 @@ db.Table("deleted_users").Find(&deleted_users) db.Table("deleted_users").Where("name = ?", "jinzhu").Delete() //// DELETE FROM deleted_users WHERE name = 'jinzhu'; ``` + +## Update + +### Update All Fields + +`Save` will include all fields when perform the Updating SQL, even it is not changed + +```go +db.First(&user) + +user.Name = "jinzhu 2" +user.Age = 100 +db.Save(&user) + +//// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111; +``` + +### Update Changed Fields + +If you only want to update changed Fields, you could use `Update`, `Updates` + +```go +// Update single attribute if it is changed +db.Model(&user).Update("name", "hello") +//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111; + +// Update single attribute with combined conditions +db.Model(&user).Where("active = ?", true).Update("name", "hello") +//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true; + + +// Update multiple attributes with `map`, will only update those changed fields +db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) +//// UPDATE users SET name='hello', age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111; + +// Update multiple attributes with `struct`, will only update those changed & non blank fields +db.Model(&user).Updates(User{Name: "hello", Age: 18}) +//// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111; + +// WARNING when update with struct, GORM will only update those fields that with non blank value +// For below Update, nothing will be updated as "", 0, false are blank values of their types +db.Model(&user).Updates(User{Name: "", Age: 0, Actived: false}) +``` + +### Update Selected Fields + +If you only want to update or ignore some fields when updating, you could use `Select`, `Omit` + +```go +db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) +//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111; + +db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) +//// UPDATE users SET age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111; +``` + +### Update Changed Fields Without Callbacks + +Updating operations above will invoke `BeforeUpdate`, `AfterUpdate`, Update UpdatedAt timestamp, Save Associations callbacks, if you don't call them, you could use `UpdateColumn`, `UpdateColumns` + +```go +// Update single attribute, similar with `Update` +db.Model(&user).UpdateColumn("name", "hello") +//// UPDATE users SET name='hello' WHERE id = 111; + +// Update multiple attributes, similar with `Updates` +db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18}) +//// UPDATE users SET name='hello', age=18 WHERE id = 111; +``` + +### Batch Updates + +Callbacks won't run when do batch updates + +```go +db.Table("users").Where("id IN (?)", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18}) +//// UPDATE users SET name='hello', age=18 WHERE id IN (10, 11); + +// Update with struct only works with none zero values, or use map[string]interface{} +db.Model(User{}).Updates(User{Name: "hello", Age: 18}) +//// UPDATE users SET name='hello', age=18; + +// Get updated records count with `RowsAffected` +db.Model(User{}).Updates(User{Name: "hello", Age: 18}).RowsAffected +``` + +### Update with SQL Expression + +```go +DB.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100)) +//// UPDATE "products" SET "price" = price * '2' + '100', "updated_at" = '2013-11-17 21:34:10' WHERE "id" = '2'; + +DB.Model(&product).Updates(map[string]interface{}{"price": gorm.Expr("price * ? + ?", 2, 100)}) +//// UPDATE "products" SET "price" = price * '2' + '100', "updated_at" = '2013-11-17 21:34:10' WHERE "id" = '2'; + +DB.Model(&product).UpdateColumn("quantity", gorm.Expr("quantity - ?", 1)) +//// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = '2'; + +DB.Model(&product).Where("quantity > 1").UpdateColumn("quantity", gorm.Expr("quantity - ?", 1)) +//// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = '2' AND quantity > 1; +``` + +### Change Updating Values In Callbacks + +If you want to change updating values in callbacks using `BeforeUpdate`, `BeforeSave`, you could use `scope.SetColumn`, for example: + +```go +func (user *User) BeforeSave(scope *gorm.Scope) (err error) { + if pw, err := bcrypt.GenerateFromPassword(user.Password, 0); err == nil { + scope.SetColumn("EncryptedPassword", pw) + } +} +``` + +### Extra Updating option + +```go +// Add extra SQL option for updating SQL +db.Model(&user).Set("gorm:update_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Update("name, "hello") +//// UPDATE users SET name='hello', updated_at = '2013-11-17 21:34:10' WHERE id=111 OPTION (OPTIMIZE FOR UNKNOWN); +``` + +## Delete + +```go +// Delete an existing record +db.Delete(&email) +//// DELETE from emails where id=10; + +// Add extra SQL option for deleting SQL +db.Set("gorm:delete_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Delete(&email) +//// DELETE from emails where id=10 OPTION (OPTIMIZE FOR UNKNOWN); +``` + +### Batch Delete + +```go +db.Where("email LIKE ?", "%jinzhu%").Delete(Email{}) +//// DELETE from emails where email LIKE "%jinhu%"; +``` + +### Soft Delete + +If struct has `DeletedAt` field, it will get soft delete ability automatically! +Then it won't be deleted from database permanently when call `Delete`. + +```go +db.Delete(&user) +//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111; + +// Batch Delete +db.Where("age = ?", 20).Delete(&User{}) +//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20; + +// Soft deleted records will be ignored when query them +db.Where("age = 20").Find(&user) +//// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL; + +// Find soft deleted records with Unscoped +db.Unscoped().Where("age = 20").Find(&users) +//// SELECT * FROM users WHERE age = 20; + +// Delete record permanently with Unscoped +db.Unscoped().Delete(&order) +//// DELETE FROM orders WHERE id=10; +``` diff --git a/doc/curd/create.md b/doc/curd/create.md deleted file mode 100644 index 614588f2..00000000 --- a/doc/curd/create.md +++ /dev/null @@ -1,76 +0,0 @@ -# Create - -### Create Record - -```go -user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()} - -db.NewRecord(user) // => returns `true` as primary key is blank - -db.Create(&user) - -db.NewRecord(user) // => return `false` after `user` created - -// Associations will be inserted automatically when save the record -user := User{ - Name: "jinzhu", - BillingAddress: Address{Address1: "Billing Address - Address 1"}, - ShippingAddress: Address{Address1: "Shipping Address - Address 1"}, - Emails: []Email{{Email: "jinzhu@example.com"}, {Email: "jinzhu-2@example@example.com"}}, - Languages: []Language{{Name: "ZH"}, {Name: "EN"}}, -} - -db.Create(&user) -//// BEGIN TRANSACTION; -//// INSERT INTO "addresses" (address1) VALUES ("Billing Address - Address 1"); -//// INSERT INTO "addresses" (address1) VALUES ("Shipping Address - Address 1"); -//// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2); -//// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com"); -//// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu-2@example.com"); -//// INSERT INTO "languages" ("name") VALUES ('ZH'); -//// INSERT INTO user_languages ("user_id","language_id") VALUES (111, 1); -//// INSERT INTO "languages" ("name") VALUES ('EN'); -//// INSERT INTO user_languages ("user_id","language_id") VALUES (111, 2); -//// COMMIT; -``` - -### Create With Associations - -Refer Associations for more details - -### Default Values - -You could defined default value in the `sql` tag, then the generated creating SQL will ignore these fields that including default value and its value is blank, and after inserted the record into databae, gorm will load those fields's value from database. - -```go -type Animal struct { - ID int64 - Name string `sql:"default:'galeone'"` - Age int64 -} - -var animal = Animal{Age: 99, Name: ""} -db.Create(&animal) -// INSERT INTO animals("age") values('99'); -// SELECT name from animals WHERE ID=111; // the returning primary key is 111 -// animal.Name => 'galeone' -``` - -### Setting Primary Key In Callbacks - -If you want to set primary key in `BeforeCreate` callback, you could use `scope.SetColumn`, for example: - -```go -func (user *User) BeforeCreate(scope *gorm.Scope) error { - scope.SetColumn("ID", uuid.New()) - return nil -} -``` - -### Extra Creating option - -```go -// Add extra SQL option for inserting SQL -db.Set("gorm:insert_option", "ON CONFLICT").Create(&product) -// INSERT INTO products (name, code) VALUES ("name", "code") ON CONFLICT; -``` diff --git a/doc/curd/curd.md b/doc/curd/curd.md deleted file mode 100644 index dd3f4dae..00000000 --- a/doc/curd/curd.md +++ /dev/null @@ -1,9 +0,0 @@ -# CRUD: Reading and Writing Data - -{% for section in book.chapters["curd/curd.md"].sections %} -* [**{{section.name}}**](../{{section.path}}) -{% if section["sections"] %}{% for subsection in section.sections %} - * [**{{ subsection.name }}**]({{ subsection.path }}) -{% endfor %}{% endif %} -{% endfor %} - diff --git a/doc/curd/delete.md b/doc/curd/delete.md deleted file mode 100644 index 353560d4..00000000 --- a/doc/curd/delete.md +++ /dev/null @@ -1,44 +0,0 @@ -# Delete - -```go -// Delete an existing record -db.Delete(&email) -//// DELETE from emails where id=10; - -// Add extra SQL option for deleting SQL -db.Set("gorm:delete_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Delete(&email) -//// DELETE from emails where id=10 OPTION (OPTIMIZE FOR UNKNOWN); -``` - -### Batch Delete - -```go -db.Where("email LIKE ?", "%jinzhu%").Delete(Email{}) -//// DELETE from emails where email LIKE "%jinhu%"; -``` - -### Soft Delete - -If struct has `DeletedAt` field, it will get soft delete ability automatically! -Then it won't be deleted from database permanently when call `Delete`. - -```go -db.Delete(&user) -//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111; - -// Batch Delete -db.Where("age = ?", 20).Delete(&User{}) -//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20; - -// Soft deleted records will be ignored when query them -db.Where("age = 20").Find(&user) -//// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL; - -// Find soft deleted records with Unscoped -db.Unscoped().Where("age = 20").Find(&users) -//// SELECT * FROM users WHERE age = 20; - -// Delete record permanently with Unscoped -db.Unscoped().Delete(&order) -//// DELETE FROM orders WHERE id=10; -``` diff --git a/doc/curd/preloading.md b/doc/curd/preloading.md deleted file mode 100644 index dec4f012..00000000 --- a/doc/curd/preloading.md +++ /dev/null @@ -1,40 +0,0 @@ -# Preloading (Eager loading) - -```go -db.Preload("Orders").Find(&users) -//// SELECT * FROM users; -//// SELECT * FROM orders WHERE user_id IN (1,2,3,4); - -db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users) -//// SELECT * FROM users; -//// SELECT * FROM orders WHERE user_id IN (1,2,3,4) AND state NOT IN ('cancelled'); - -db.Where("state = ?", "active").Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users) -//// SELECT * FROM users WHERE state = 'active'; -//// SELECT * FROM orders WHERE user_id IN (1,2) AND state NOT IN ('cancelled'); - -db.Preload("Orders").Preload("Profile").Preload("Role").Find(&users) -//// SELECT * FROM users; -//// SELECT * FROM orders WHERE user_id IN (1,2,3,4); // has many -//// SELECT * FROM profiles WHERE user_id IN (1,2,3,4); // has one -//// SELECT * FROM roles WHERE id IN (4,5,6); // belongs to -``` - -#### Custom Preloading SQL - -You could custom preloading SQL by passing in `func(db *gorm.DB) *gorm.DB` (same type as the one used for [Scopes](#scopes)), for example: - -```go -db.Preload("Orders", func(db *gorm.DB) *gorm.DB { - return db.Order("orders.amount DESC") -}).Find(&users) -//// SELECT * FROM users; -//// SELECT * FROM orders WHERE user_id IN (1,2,3,4) order by orders.amount DESC; -``` - -#### Nested Preloading - -```go -db.Preload("Orders.OrderItems").Find(&users) -db.Preload("Orders", "state = ?", "paid").Preload("Orders.OrderItems").Find(&users) -``` diff --git a/doc/curd/update.md b/doc/curd/update.md deleted file mode 100644 index 4f99b77f..00000000 --- a/doc/curd/update.md +++ /dev/null @@ -1,120 +0,0 @@ -# Update - -### Update All Fields - -`Save` will include all fields when perform the Updating SQL, even it is not changed - -```go -db.First(&user) - -user.Name = "jinzhu 2" -user.Age = 100 -db.Save(&user) - -//// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111; -``` - -### Update Changed Fields - -If you only want to update changed Fields, you could use `Update`, `Updates` - -```go -// Update single attribute if it is changed -db.Model(&user).Update("name", "hello") -//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111; - -// Update single attribute with combined conditions -db.Model(&user).Where("active = ?", true).Update("name", "hello") -//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true; - - -// Update multiple attributes with `map`, will only update those changed fields -db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) -//// UPDATE users SET name='hello', age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111; - -// Update multiple attributes with `struct`, will only update those changed & non blank fields -db.Model(&user).Updates(User{Name: "hello", Age: 18}) -//// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111; - -// WARNING when update with struct, GORM will only update those fields that with non blank value -// For below Update, nothing will be updated as "", 0, false are blank values of their types -db.Model(&user).Updates(User{Name: "", Age: 0, Actived: false}) -``` - -### Update Selected Fields - -If you only want to update or ignore some fields when updating, you could use `Select`, `Omit` - -```go -db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) -//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111; - -db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) -//// UPDATE users SET age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111; -``` - -### Update Changed Fields Without Callbacks - -Updating operations above will invoke `BeforeUpdate`, `AfterUpdate`, Update UpdatedAt timestamp, Save Associations callbacks, if you don't call them, you could use `UpdateColumn`, `UpdateColumns` - -```go -// Update single attribute, similar with `Update` -db.Model(&user).UpdateColumn("name", "hello") -//// UPDATE users SET name='hello' WHERE id = 111; - -// Update multiple attributes, similar with `Updates` -db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18}) -//// UPDATE users SET name='hello', age=18 WHERE id = 111; -``` - -### Batch Updates - -Callbacks won't run when do batch updates - -```go -db.Table("users").Where("id IN (?)", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18}) -//// UPDATE users SET name='hello', age=18 WHERE id IN (10, 11); - -// Update with struct only works with none zero values, or use map[string]interface{} -db.Model(User{}).Updates(User{Name: "hello", Age: 18}) -//// UPDATE users SET name='hello', age=18; - -// Get updated records count with `RowsAffected` -db.Model(User{}).Updates(User{Name: "hello", Age: 18}).RowsAffected -``` - -### Update with SQL Expression - -```go -DB.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100)) -//// UPDATE "products" SET "price" = price * '2' + '100', "updated_at" = '2013-11-17 21:34:10' WHERE "id" = '2'; - -DB.Model(&product).Updates(map[string]interface{}{"price": gorm.Expr("price * ? + ?", 2, 100)}) -//// UPDATE "products" SET "price" = price * '2' + '100', "updated_at" = '2013-11-17 21:34:10' WHERE "id" = '2'; - -DB.Model(&product).UpdateColumn("quantity", gorm.Expr("quantity - ?", 1)) -//// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = '2'; - -DB.Model(&product).Where("quantity > 1").UpdateColumn("quantity", gorm.Expr("quantity - ?", 1)) -//// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = '2' AND quantity > 1; -``` - -### Change Updating Values In Callbacks - -If you want to change updating values in callbacks using `BeforeUpdate`, `BeforeSave`, you could use `scope.SetColumn`, for example: - -```go -func (user *User) BeforeSave(scope *gorm.Scope) (err error) { - if pw, err := bcrypt.GenerateFromPassword(user.Password, 0); err == nil { - scope.SetColumn("EncryptedPassword", pw) - } -} -``` - -### Extra Updating option - -```go -// Add extra SQL option for updating SQL -db.Model(&user).Set("gorm:update_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Update("name, "hello") -//// UPDATE users SET name='hello', updated_at = '2013-11-17 21:34:10' WHERE id=111 OPTION (OPTIMIZE FOR UNKNOWN); -``` diff --git a/doc/database/migration.md b/doc/database.md similarity index 59% rename from doc/database/migration.md rename to doc/database.md index 0ca27a13..98b6077d 100644 --- a/doc/database/migration.md +++ b/doc/database.md @@ -1,3 +1,70 @@ +# Database + + + +## Connecting to a database + +#### MySQL + +**NOTE** don't forgot params `parseTime` to handle data type `time.Time`, [more support parameters](https://github.com/go-sql-driver/mysql#parameters) + +```go +import ( + "github.com/jinzhu/gorm" + _ "github.com/go-sql-driver/mysql" +) +func main() { + db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local") +} +``` + +#### PostgreSQL + +```go +import ( + "github.com/jinzhu/gorm" + _ "github.com/lib/pq" +) +func main() { + db, err := gorm.Open("postgres", "user=gorm dbname=gorm sslmode=disable") +} +``` + +#### Sqlite3 + +```go +import ( + "github.com/jinzhu/gorm" + _ "github.com/mattn/go-sqlite3" +) +func main() { + db, err := gorm.Open("sqlite3", "/tmp/gorm.db") +} +``` + +#### Write Dialect for unsupported databases + +GORM officially support above databases, for unsupported databaes, you could write a dialect for that. + +Refer: https://github.com/jinzhu/gorm/blob/master/dialect.go + + +## Generic database object *sql.DB + +[*sql.DB](http://golang.org/pkg/database/sql/#DB) + +```go +// Get generic database object *sql.DB to use its functions +db.DB() + +// Connection Pool +db.DB().SetMaxIdleConns(10) +db.DB().SetMaxOpenConns(100) + + // Ping +db.DB().Ping() +``` + ## Migration diff --git a/doc/database/connect-database.md b/doc/database/connect-database.md deleted file mode 100644 index 66f04509..00000000 --- a/doc/database/connect-database.md +++ /dev/null @@ -1,66 +0,0 @@ -# Database Connection - - - -## Connecting to a database - -#### MySQL - -**NOTE** don't forgot params `parseTime` to handle data type `time.Time`, [more support parameters](https://github.com/go-sql-driver/mysql#parameters) - -```go -import ( - "github.com/jinzhu/gorm" - _ "github.com/go-sql-driver/mysql" -) -func main() { - db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local") -} -``` - -#### PostgreSQL - -```go -import ( - "github.com/jinzhu/gorm" - _ "github.com/lib/pq" -) -func main() { - db, err := gorm.Open("postgres", "user=gorm dbname=gorm sslmode=disable") -} -``` - -#### Sqlite3 - -```go -import ( - "github.com/jinzhu/gorm" - _ "github.com/mattn/go-sqlite3" -) -func main() { - db, err := gorm.Open("sqlite3", "/tmp/gorm.db") -} -``` - -#### Write Dialect for unsupported databases - -GORM officially support above databases, for unsupported databaes, you could write a dialect for that. - -Refer: https://github.com/jinzhu/gorm/blob/master/dialect.go - - -## Generic database object *sql.DB - -[*sql.DB](http://golang.org/pkg/database/sql/#DB) - -```go -// Get generic database object *sql.DB to use its functions -db.DB() - -// Connection Pool -db.DB().SetMaxIdleConns(10) -db.DB().SetMaxOpenConns(100) - - // Ping -db.DB().Ping() -``` diff --git a/doc/database/database.md b/doc/database/database.md deleted file mode 100644 index 0733f61b..00000000 --- a/doc/database/database.md +++ /dev/null @@ -1,5 +0,0 @@ -# Database - -{% for section in book.chapters["database/database.md"].sections %} -* [**{{section.name}}**](../{{section.path}}) -{% endfor %} diff --git a/doc/development/development.md b/doc/development.md similarity index 99% rename from doc/development/development.md rename to doc/development.md index 08166661..021cc1f9 100644 --- a/doc/development/development.md +++ b/doc/development.md @@ -1,5 +1,7 @@ # Gorm Development + + ## Architecture The most notable component of Gorm is`gorm.DB`, which hold database connection. It could be initialized like this: diff --git a/doc/models/defination.md b/doc/models.md similarity index 100% rename from doc/models/defination.md rename to doc/models.md diff --git a/doc/models/models.md b/doc/models/models.md deleted file mode 100644 index 46abf2da..00000000 --- a/doc/models/models.md +++ /dev/null @@ -1,8 +0,0 @@ -# Models - -{% for section in book.chapters["models/models.md"].sections %} -* [**{{section.name}}**](../{{section.path}}) -{% if section["sections"] %}{% for subsection in section.sections %} - * [**{{ subsection.name }}**]({{ subsection.path }}) -{% endfor %}{% endif %} -{% endfor %} diff --git a/doc/models/naming-conventions.md b/doc/models/naming-conventions.md deleted file mode 100644 index e69de29b..00000000 From 449ff0eca734f0f787906dd49ada4e008f8b1e4d Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 28 Feb 2016 00:01:08 +0800 Subject: [PATCH 66/83] Update book.json --- doc/book.json | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/doc/book.json b/doc/book.json index 138e9782..46d7daea 100644 --- a/doc/book.json +++ b/doc/book.json @@ -1,10 +1,15 @@ { "title": "GORM Guide", "plugins": [ - "prism", "-highlight", "toc", - "github", "edit-link" + "-highlight", "prism", "toc", "hide-element", + "github", "edit-link", "anker-enable" ], "pluginsConfig": { + "fontsettings": { + "theme": "night", + "family": "sans", + "size": 4 + }, "toc": { "addClass": true, "className": "toc" @@ -15,12 +20,9 @@ "edit-link": { "base": "https://github.com/jinzhu/gorm/edit/master", "label": "Edit This Page" + }, + "hide-element": { + "elements": [".gitbook-link"] } - }, - "sections": [ - { - "content": "

Hello

", - "type": "normal" - } - ] + } } From cd766809c656218a8e34c8935a757c1c068b9555 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 29 Feb 2016 22:06:15 +0800 Subject: [PATCH 67/83] Update README --- .gitignore | 2 + CHANGELOG.md | 2 +- README.md | 10 +- doc/.gitignore | 3 - doc/README.md | 74 ----- doc/SUMMARY.md | 31 -- doc/advanced.md | 149 ---------- doc/associations.md | 150 ---------- doc/book.json | 28 -- doc/callbacks.md | 78 ----- doc/curd.md | 674 -------------------------------------------- doc/database.md | 156 ---------- doc/development.md | 70 ----- doc/logger.png | Bin 66982 -> 0 bytes doc/models.md | 111 -------- 15 files changed, 9 insertions(+), 1529 deletions(-) create mode 100644 .gitignore delete mode 100644 doc/.gitignore delete mode 100644 doc/README.md delete mode 100644 doc/SUMMARY.md delete mode 100644 doc/advanced.md delete mode 100644 doc/associations.md delete mode 100644 doc/book.json delete mode 100644 doc/callbacks.md delete mode 100644 doc/curd.md delete mode 100644 doc/database.md delete mode 100644 doc/development.md delete mode 100644 doc/logger.png delete mode 100644 doc/models.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..01dc5ce0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +documents +_book diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c37136e..528fe1b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -## v1.0 (unreleased) +## v1.0 #### Breaking Changes diff --git a/README.md b/README.md index a0a18330..b3388f19 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,13 @@ The fantastic ORM library for Golang, aims to be developer friendly. * Every feature comes with tests * Developer Friendly -## Install +## Getting Started -``` -go get -u github.com/jinzhu/gorm -``` +* GORM Guides [jinzhu.github.com/gorm](https://jinzhu.github.com/gorm) + +## Upgrading To V1.0 + +* [CHANGELOG]() # Author diff --git a/doc/.gitignore b/doc/.gitignore deleted file mode 100644 index a6fc88a6..00000000 --- a/doc/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/_book -/node_modules -/gitbook diff --git a/doc/README.md b/doc/README.md deleted file mode 100644 index 143b44c1..00000000 --- a/doc/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# GORM - -The fantastic ORM library for Golang, aims to be developer friendly. - -[![wercker status](https://app.wercker.com/status/0cb7bb1039e21b74f8274941428e0921/s/master "wercker status")](https://app.wercker.com/project/bykey/0cb7bb1039e21b74f8274941428e0921) -[![GoDoc](https://godoc.org/github.com/jinzhu/gorm?status.svg)](https://godoc.org/github.com/jinzhu/gorm) -[![Join the chat at https://gitter.im/jinzhu/gorm](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jinzhu/gorm?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - -## Overview - -* Full-Featured ORM (almost) -* Chainable API -* Auto Migrations -* Relations (Has One, Has Many, Belongs To, Many To Many, [Polymorphism](#polymorphism)) -* Callbacks (Before/After Create/Save/Update/Delete/Find) -* Preloading (eager loading) -* Transactions -* Embed Anonymous Struct -* Soft Deletes -* Customizable Logger -* Iteration Support via [Rows](#row--rows) -* Every feature comes with tests -* Developer Friendly - -## Install - -``` -go get -u github.com/jinzhu/gorm -``` - -## Basic Usage - -```go -type Product struct { - gorm.Model - Code string - Price uint -} - -var db *gorm.DB - -func init() { - var err error - db, err = gorm.Open("sqlite", "test.db") -} - -func main() { - db.Create(&Product{Code: "L1212", Price: 1000}) - - var product Product - db.First(&product, 1) // find product with id 1 - db.First(&product, "code = ?", "L1212") // find product with code l1212 - - db.Model(&product).Update("Price", 2000) // update product's price to 2000 - - db.Delete(&product) // delete product -} -``` - -# Author - -**jinzhu** - -* -* -* - -# Contributors - -https://github.com/jinzhu/gorm/graphs/contributors - -## License - -Released under the [MIT License](https://github.com/jinzhu/gorm/blob/master/License). diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md deleted file mode 100644 index dc76398f..00000000 --- a/doc/SUMMARY.md +++ /dev/null @@ -1,31 +0,0 @@ -# Summary - -* [Getting Started with GORM](README.md) -* [Database](database.md) - * [Database Connection](database.md#connecting-to-a-database) - * [Migration](database.md#migration) -* [Models](models.md) - * [Model Defination](models.md#model-defination), - * [Naming Conventions & Overriding](models.md#conventions-overriding-conventions), - * [Associations](associations.md) - * [Belongs To](associations.md#belongs-to) - * [Has One](associations.md#has-one) - * [Has Many](associations.md#has-many) - * [Many To Many](associations.md#many-to-many) - * [Polymorphism](associations.md#polymorphism) - * [Association Mode](associations.md#association-mode) -* [CRUD: Reading and Writing Data](curd.md) - * [Create](curd.md#create), - * [Query](curd.md#query), - * [Preloading (Eager Loading)](curd.md#preloading), - * [Update](curd.md#update), - * [Delete / Soft Delete](curd.md#delete) -* [Callbacks](callbacks.md) -* [Advanced Usage](advanced.md) - * [Error Handling](advanced.md#error-handling) - * [Transactions](advanced.md#transactions) - * [Raw SQL & SQL Builder](advanced.md#sql-builder) - * [Composite Primary Key](advanced.md#compose-primary-key) - * [Overriding Logger](advanced.md#logger) -* [Development](development.md) - * [Write Plugins](development.md#callbacks) diff --git a/doc/advanced.md b/doc/advanced.md deleted file mode 100644 index 0d8c5827..00000000 --- a/doc/advanced.md +++ /dev/null @@ -1,149 +0,0 @@ -# Advanced Usage - - - -## Error Handling - -```go -query := db.Where("name = ?", "jinzhu").First(&user) -query := db.First(&user).Limit(10).Find(&users) -// query.Error will return the last happened error - -// So you could do error handing in your application like this: -if err := db.Where("name = ?", "jinzhu").First(&user).Error; err != nil { - // error handling... -} - -// RecordNotFound -// If no record found when you query data, gorm will return RecordNotFound error, you could check it like this: -db.Where("name = ?", "hello world").First(&User{}).Error == gorm.RecordNotFound -// Or use the shortcut method -db.Where("name = ?", "hello world").First(&user).RecordNotFound() - -if db.Model(&user).Related(&credit_card).RecordNotFound() { - // no credit card found error handling -} -``` - -## Transactions - -To perform a set of operations within a transaction, the general flow is as below. -The database handle returned from ``` db.Begin() ``` should be used for all operations within the transaction. -(Note that all individual save and delete operations are run in a transaction by default.) - -```go -// begin -tx := db.Begin() - -// do some database operations (use 'tx' from this point, not 'db') -tx.Create(...) -... - -// rollback in case of error -tx.Rollback() - -// Or commit if all is ok -tx.Commit() -``` - -### A Specific Example - -```go -func CreateAnimals(db *gorm.DB) err { - tx := db.Begin() - // Note the use of tx as the database handle once you are within a transaction - - if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil { - tx.Rollback() - return err - } - - if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil { - tx.Rollback() - return err - } - - tx.Commit() - return nil -} -``` - -## Raw SQL - -```go -db.Exec("DROP TABLE users;") -db.Exec("UPDATE orders SET shipped_at=? WHERE id IN (?)", time.Now, []int64{11,22,33}) -``` - -## Row & Rows - -It is even possible to get query result as `*sql.Row` or `*sql.Rows` - -```go -row := db.Table("users").Where("name = ?", "jinzhu").Select("name, age").Row() // (*sql.Row) -row.Scan(&name, &age) - -rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error) -defer rows.Close() -for rows.Next() { - ... - rows.Scan(&name, &age, &email) - ... -} - -// Raw SQL -rows, err := db.Raw("select name, age, email from users where name = ?", "jinzhu").Rows() // (*sql.Rows, error) -defer rows.Close() -for rows.Next() { - ... - rows.Scan(&name, &age, &email) - ... -} -``` - -### Scan Rows - -```go -rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error) -defer rows.Close() - -for rows.Next() { - var user User - db.ScanRows(rows, &user) - // do something -} -``` - -## Composite Primary Key - -```go -type Product struct { - ID string `gorm:"primary_key"` - LanguageCode string `gorm:"primary_key"` -} -``` - -## Logger - -Gorm has built-in logger support - -```go -// Enable Logger -db.LogMode(true) - -// Diable Logger -db.LogMode(false) - -// Debug a single operation -db.Debug().Where("name = ?", "jinzhu").First(&User{}) -``` - -![logger](https://raw.github.com/jinzhu/gorm/master/doc/logger.png) - -### Customize Logger - -```go -// Refer gorm's default logger for how to: https://github.com/jinzhu/gorm/blob/master/logger.go#files -db.SetLogger(gorm.Logger{revel.TRACE}) -db.SetLogger(log.New(os.Stdout, "\r\n", 0)) -``` diff --git a/doc/associations.md b/doc/associations.md deleted file mode 100644 index 74e684c9..00000000 --- a/doc/associations.md +++ /dev/null @@ -1,150 +0,0 @@ -# Associations - - - -## Belongs To - -```go -// User belongs to a profile, ProfileID is the foreign key -type User struct { - gorm.Model - Profile Profile - ProfileID int -} - -type Profile struct { - gorm.Model - Name string -} - -db.Model(&user).Related(&profile) -//// SELECT * FROM profiles WHERE id = 111; // 111 is user's foreign key ProfileID -``` - -## Has One - -```go -// User has one CreditCard, UserID is the foreign key -type User struct { - gorm.Model - CreditCard CreditCard -} - -type CreditCard struct { - gorm.Model - UserID uint - Number string -} - -var card CreditCard -db.Model(&user).Related(&card, "CreditCard") -//// SELECT * FROM credit_cards WHERE user_id = 123; // 123 is user's primary key -// CreditCard is user's field name, it means get user's CreditCard relations and fill it into variable card -// If the field name is same as the variable's type name, like above example, it could be omitted, like: -db.Model(&user).Related(&card) -``` - -## Has Many - -```go -// User has many emails, UserID is the foreign key -type User struct { - gorm.Model - Emails []Email -} - -type Email struct { - gorm.Model - Email string - UserID uint -} - -db.Model(&user).Related(&emails) -//// SELECT * FROM emails WHERE user_id = 111; // 111 is user's primary key -``` - -## Many To Many - -```go -// User has and belongs to many languages, use `user_languages` as join table -type User struct { - gorm.Model - Languages []Language `gorm:"many2many:user_languages;"` -} - -type Language struct { - gorm.Model - Name string -} - -db.Model(&user).Related(&languages) -//// SELECT * FROM "languages" INNER JOIN "user_languages" ON "user_languages"."language_id" = "languages"."id" WHERE "user_languages"."user_id" = 111 -``` - -## Polymorphism - -Supports polymorphic has-many and has-one associations. - -```go - type Cat struct { - Id int - Name string - Toy Toy `gorm:"polymorphic:Owner;"` - } - - type Dog struct { - Id int - Name string - Toy Toy `gorm:"polymorphic:Owner;"` - } - - type Toy struct { - Id int - Name string - OwnerId int - OwnerType string - } -``` - -Note: polymorphic belongs-to and many-to-many are explicitly NOT supported, and will throw errors. - -## Association Mode - -Association Mode contains some helper methods to handle relationship things easily. - -```go -// Start Association Mode -var user User -db.Model(&user).Association("Languages") -// `user` is the source, it need to be a valid record (contains primary key) -// `Languages` is source's field name for a relationship. -// If those conditions not matched, will return an error, check it with: -// db.Model(&user).Association("Languages").Error - - -// Query - Find out all related associations -db.Model(&user).Association("Languages").Find(&languages) - - -// Append - Append new associations for many2many, has_many, will replace current association for has_one, belongs_to -db.Model(&user).Association("Languages").Append([]Language{languageZH, languageEN}) -db.Model(&user).Association("Languages").Append(Language{Name: "DE"}) - - -// Delete - Remove relationship between source & passed arguments, won't delete those arguments -db.Model(&user).Association("Languages").Delete([]Language{languageZH, languageEN}) -db.Model(&user).Association("Languages").Delete(languageZH, languageEN) - - -// Replace - Replace current associations with new one -db.Model(&user).Association("Languages").Replace([]Language{languageZH, languageEN}) -db.Model(&user).Association("Languages").Replace(Language{Name: "DE"}, languageEN) - - -// Count - Return the count of current associations -db.Model(&user).Association("Languages").Count() - - -// Clear - Remove relationship between source & current associations, won't delete those associations -db.Model(&user).Association("Languages").Clear() -``` diff --git a/doc/book.json b/doc/book.json deleted file mode 100644 index 46d7daea..00000000 --- a/doc/book.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "title": "GORM Guide", - "plugins": [ - "-highlight", "prism", "toc", "hide-element", - "github", "edit-link", "anker-enable" - ], - "pluginsConfig": { - "fontsettings": { - "theme": "night", - "family": "sans", - "size": 4 - }, - "toc": { - "addClass": true, - "className": "toc" - }, - "github": { - "url": "https://github.com/jinzhu/gorm" - }, - "edit-link": { - "base": "https://github.com/jinzhu/gorm/edit/master", - "label": "Edit This Page" - }, - "hide-element": { - "elements": [".gitbook-link"] - } - } -} diff --git a/doc/callbacks.md b/doc/callbacks.md deleted file mode 100644 index 18ef4cca..00000000 --- a/doc/callbacks.md +++ /dev/null @@ -1,78 +0,0 @@ -# Callbacks - - - -Callbacks are methods defined on the pointer of struct. -If any callback returns an error, gorm will stop future operations and rollback all changes. - -Here is the list of all available callbacks: -(listed in the same order in which they will get called during the respective operations) - -### Creating An Object - -```go -BeforeSave -BeforeCreate -// save before associations -// save self -// save after associations -AfterCreate -AfterSave -``` -### Updating An Object - -```go -BeforeSave -BeforeUpdate -// save before associations -// save self -// save after associations -AfterUpdate -AfterSave -``` - -### Destroying An Object - -```go -BeforeDelete -// delete self -AfterDelete -``` - -### After Find - -```go -// load data from database -AfterFind -``` - -### Example - -```go -func (u *User) BeforeUpdate() (err error) { - if u.readonly() { - err = errors.New("read only user") - } - return -} - -// Rollback the insertion if user's id greater than 1000 -func (u *User) AfterCreate() (err error) { - if (u.Id > 1000) { - err = errors.New("user id is already greater than 1000") - } - return -} -``` - -Save/delete operations in gorm are running in a transaction. -Changes made in that transaction are not visible unless it is commited. -So if you want to use those changes in your callbacks, you need to run your SQL in the same transaction. -For this Gorm supports passing transactions to callbacks like this: - -```go -func (u *User) AfterCreate(tx *gorm.DB) (err error) { - tx.Model(u).Update("role", "admin") - return -} -``` diff --git a/doc/curd.md b/doc/curd.md deleted file mode 100644 index 998c1fb9..00000000 --- a/doc/curd.md +++ /dev/null @@ -1,674 +0,0 @@ -# CRUD: Reading and Writing Data - - - -## Create - -### Create Record - -```go -user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()} - -db.NewRecord(user) // => returns `true` as primary key is blank - -db.Create(&user) - -db.NewRecord(user) // => return `false` after `user` created - -// Associations will be inserted automatically when save the record -user := User{ - Name: "jinzhu", - BillingAddress: Address{Address1: "Billing Address - Address 1"}, - ShippingAddress: Address{Address1: "Shipping Address - Address 1"}, - Emails: []Email{{Email: "jinzhu@example.com"}, {Email: "jinzhu-2@example@example.com"}}, - Languages: []Language{{Name: "ZH"}, {Name: "EN"}}, -} - -db.Create(&user) -//// BEGIN TRANSACTION; -//// INSERT INTO "addresses" (address1) VALUES ("Billing Address - Address 1"); -//// INSERT INTO "addresses" (address1) VALUES ("Shipping Address - Address 1"); -//// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2); -//// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com"); -//// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu-2@example.com"); -//// INSERT INTO "languages" ("name") VALUES ('ZH'); -//// INSERT INTO user_languages ("user_id","language_id") VALUES (111, 1); -//// INSERT INTO "languages" ("name") VALUES ('EN'); -//// INSERT INTO user_languages ("user_id","language_id") VALUES (111, 2); -//// COMMIT; -``` - -### Create With Associations - -Refer Associations for more details - -### Default Values - -You could defined default value in the `sql` tag, then the generated creating SQL will ignore these fields that including default value and its value is blank, and after inserted the record into databae, gorm will load those fields's value from database. - -```go -type Animal struct { - ID int64 - Name string `sql:"default:'galeone'"` - Age int64 -} - -var animal = Animal{Age: 99, Name: ""} -db.Create(&animal) -// INSERT INTO animals("age") values('99'); -// SELECT name from animals WHERE ID=111; // the returning primary key is 111 -// animal.Name => 'galeone' -``` - -### Setting Primary Key In Callbacks - -If you want to set primary key in `BeforeCreate` callback, you could use `scope.SetColumn`, for example: - -```go -func (user *User) BeforeCreate(scope *gorm.Scope) error { - scope.SetColumn("ID", uuid.New()) - return nil -} -``` - -### Extra Creating option - -```go -// Add extra SQL option for inserting SQL -db.Set("gorm:insert_option", "ON CONFLICT").Create(&product) -// INSERT INTO products (name, code) VALUES ("name", "code") ON CONFLICT; -``` - - -## Query - -```go -// Get the first record -db.First(&user) -//// SELECT * FROM users ORDER BY id LIMIT 1; - -// Get the last record -db.Last(&user) -//// SELECT * FROM users ORDER BY id DESC LIMIT 1; - -// Get all records -db.Find(&users) -//// SELECT * FROM users; - -// Get record with primary key -db.First(&user, 10) -//// SELECT * FROM users WHERE id = 10; -``` - -### Query With Where (Plain SQL) - -```go -// Get the first matched record -db.Where("name = ?", "jinzhu").First(&user) -//// SELECT * FROM users WHERE name = 'jinzhu' limit 1; - -// Get all matched records -db.Where("name = ?", "jinzhu").Find(&users) -//// SELECT * FROM users WHERE name = 'jinzhu'; - -db.Where("name <> ?", "jinzhu").Find(&users) - -// IN -db.Where("name in (?)", []string{"jinzhu", "jinzhu 2"}).Find(&users) - -// LIKE -db.Where("name LIKE ?", "%jin%").Find(&users) - -// AND -db.Where("name = ? and age >= ?", "jinzhu", "22").Find(&users) - -// Time -db.Where("updated_at > ?", lastWeek).Find(&users) - -db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users) -``` - -### Query With Where (Struct & Map) - -```go -// Struct -db.Where(&User{Name: "jinzhu", Age: 20}).First(&user) -//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 LIMIT 1; - -// Map -db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users) -//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20; - -// Slice of primary keys -db.Where([]int64{20, 21, 22}).Find(&users) -//// SELECT * FROM users WHERE id IN (20, 21, 22); -``` - -### Query With Not - -```go -db.Not("name", "jinzhu").First(&user) -//// SELECT * FROM users WHERE name <> "jinzhu" LIMIT 1; - -// Not In -db.Not("name", []string{"jinzhu", "jinzhu 2"}).Find(&users) -//// SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2"); - -// Not In slice of primary keys -db.Not([]int64{1,2,3}).First(&user) -//// SELECT * FROM users WHERE id NOT IN (1,2,3); - -db.Not([]int64{}).First(&user) -//// SELECT * FROM users; - -// Plain SQL -db.Not("name = ?", "jinzhu").First(&user) -//// SELECT * FROM users WHERE NOT(name = "jinzhu"); - -// Struct -db.Not(User{Name: "jinzhu"}).First(&user) -//// SELECT * FROM users WHERE name <> "jinzhu"; -``` - -### Query With Inline Condition - -```go -// Get by primary key -db.First(&user, 23) -//// SELECT * FROM users WHERE id = 23 LIMIT 1; - -// Plain SQL -db.Find(&user, "name = ?", "jinzhu") -//// SELECT * FROM users WHERE name = "jinzhu"; - -db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20) -//// SELECT * FROM users WHERE name <> "jinzhu" AND age > 20; - -// Struct -db.Find(&users, User{Age: 20}) -//// SELECT * FROM users WHERE age = 20; - -// Map -db.Find(&users, map[string]interface{}{"age": 20}) -//// SELECT * FROM users WHERE age = 20; -``` - -### Query With Or - -```go -db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users) -//// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin'; - -// Struct -db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2"}).Find(&users) -//// SELECT * FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'; - -// Map -db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2"}).Find(&users) -``` - -### Query Chains - -Gorm has a chainable API, you could use it like this - -```go -db.Where("name <> ?","jinzhu").Where("age >= ? and role <> ?",20,"admin").Find(&users) -//// SELECT * FROM users WHERE name <> 'jinzhu' AND age >= 20 AND role <> 'admin'; - -db.Where("role = ?", "admin").Or("role = ?", "super_admin").Not("name = ?", "jinzhu").Find(&users) -``` - -### Extra Querying option - -```go -// Add extra SQL option for selecting SQL -db.Set("gorm:query_option", "FOR UPDATE").First(&user, 10) -//// SELECT * FROM users WHERE id = 10 FOR UPDATE; -``` - -### FirstOrInit - -Get the first matched record, or initialize a record with search conditions. - -```go -// Unfound -db.FirstOrInit(&user, User{Name: "non_existing"}) -//// user -> User{Name: "non_existing"} - -// Found -db.Where(User{Name: "Jinzhu"}).FirstOrInit(&user) -//// user -> User{Id: 111, Name: "Jinzhu", Age: 20} -db.FirstOrInit(&user, map[string]interface{}{"name": "jinzhu"}) -//// user -> User{Id: 111, Name: "Jinzhu", Age: 20} -``` - -#### Attrs - -Ignore some values when searching, but use them to initialize the struct if record is not found. - -```go -// Unfound -db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrInit(&user) -//// SELECT * FROM USERS WHERE name = 'non_existing'; -//// user -> User{Name: "non_existing", Age: 20} - -db.Where(User{Name: "noexisting_user"}).Attrs("age", 20).FirstOrInit(&user) -//// SELECT * FROM USERS WHERE name = 'non_existing'; -//// user -> User{Name: "non_existing", Age: 20} - -// Found -db.Where(User{Name: "Jinzhu"}).Attrs(User{Age: 30}).FirstOrInit(&user) -//// SELECT * FROM USERS WHERE name = jinzhu'; -//// user -> User{Id: 111, Name: "Jinzhu", Age: 20} -``` - -#### Assign - -Ignore some values when searching, but assign it to the result regardless it is found or not. - -```go -// Unfound -db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrInit(&user) -//// user -> User{Name: "non_existing", Age: 20} - -// Found -db.Where(User{Name: "Jinzhu"}).Assign(User{Age: 30}).FirstOrInit(&user) -//// SELECT * FROM USERS WHERE name = jinzhu'; -//// user -> User{Id: 111, Name: "Jinzhu", Age: 30} -``` - -### FirstOrCreate - -Get the first matched record, or create with search conditions. - -```go -// Unfound -db.FirstOrCreate(&user, User{Name: "non_existing"}) -//// INSERT INTO "users" (name) VALUES ("non_existing"); -//// user -> User{Id: 112, Name: "non_existing"} - -// Found -db.Where(User{Name: "Jinzhu"}).FirstOrCreate(&user) -//// user -> User{Id: 111, Name: "Jinzhu"} -``` - -#### Attrs - -Ignore some values when searching, but use them to create the struct if record is not found. like `FirstOrInit` - -```go -// Unfound -db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrCreate(&user) -//// SELECT * FROM users WHERE name = 'non_existing'; -//// INSERT INTO "users" (name, age) VALUES ("non_existing", 20); -//// user -> User{Id: 112, Name: "non_existing", Age: 20} - -// Found -db.Where(User{Name: "jinzhu"}).Attrs(User{Age: 30}).FirstOrCreate(&user) -//// SELECT * FROM users WHERE name = 'jinzhu'; -//// user -> User{Id: 111, Name: "jinzhu", Age: 20} -``` - -#### Assign - -Ignore some values when searching, but assign it to the record regardless it is found or not, then save back to database. like `FirstOrInit` - -```go -// Unfound -db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrCreate(&user) -//// SELECT * FROM users WHERE name = 'non_existing'; -//// INSERT INTO "users" (name, age) VALUES ("non_existing", 20); -//// user -> User{Id: 112, Name: "non_existing", Age: 20} - -// Found -db.Where(User{Name: "jinzhu"}).Assign(User{Age: 30}).FirstOrCreate(&user) -//// SELECT * FROM users WHERE name = 'jinzhu'; -//// UPDATE users SET age=30 WHERE id = 111; -//// user -> User{Id: 111, Name: "jinzhu", Age: 30} -``` - -### Select - -```go -db.Select("name, age").Find(&users) -//// SELECT name, age FROM users; - -db.Select([]string{"name", "age"}).Find(&users) -//// SELECT name, age FROM users; - -db.Table("users").Select("COALESCE(age,?)", 42).Rows() -//// SELECT COALESCE(age,'42') FROM users; -``` - -### Order - -```go -db.Order("age desc, name").Find(&users) -//// SELECT * FROM users ORDER BY age desc, name; - -// Multiple orders -db.Order("age desc").Order("name").Find(&users) -//// SELECT * FROM users ORDER BY age desc, name; - -// ReOrder -db.Order("age desc").Find(&users1).Order("age", true).Find(&users2) -//// SELECT * FROM users ORDER BY age desc; (users1) -//// SELECT * FROM users ORDER BY age; (users2) -``` - -### Limit - -```go -db.Limit(3).Find(&users) -//// SELECT * FROM users LIMIT 3; - -// Cancel limit condition with -1 -db.Limit(10).Find(&users1).Limit(-1).Find(&users2) -//// SELECT * FROM users LIMIT 10; (users1) -//// SELECT * FROM users; (users2) -``` - -### Offset - -```go -db.Offset(3).Find(&users) -//// SELECT * FROM users OFFSET 3; - -// Cancel offset condition with -1 -db.Offset(10).Find(&users1).Offset(-1).Find(&users2) -//// SELECT * FROM users OFFSET 10; (users1) -//// SELECT * FROM users; (users2) -``` - -### Count - -```go -db.Where("name = ?", "jinzhu").Or("name = ?", "jinzhu 2").Find(&users).Count(&count) -//// SELECT * from USERS WHERE name = 'jinzhu' OR name = 'jinzhu 2'; (users) -//// SELECT count(*) FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'; (count) - -db.Model(&User{}).Where("name = ?", "jinzhu").Count(&count) -//// SELECT count(*) FROM users WHERE name = 'jinzhu'; (count) - -db.Table("deleted_users").Count(&count) -//// SELECT count(*) FROM deleted_users; -``` - -### Group & Having - -```go -rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Rows() -for rows.Next() { - ... -} - -rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Rows() -for rows.Next() { - ... -} - -type Result struct { - Date time.Time - Total int64 -} -db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Scan(&results) -``` - -### Joins - -```go -rows, err := db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Rows() -for rows.Next() { - ... -} - -db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results) - -// multiple joins with parameter -db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?", "411111111111").Find(&user) -``` - -### Pluck - -Get selected attributes as map - -```go -var ages []int64 -db.Find(&users).Pluck("age", &ages) - -var names []string -db.Model(&User{}).Pluck("name", &names) - -db.Table("deleted_users").Pluck("name", &names) - -// Requesting more than one column? Do it like this: -db.Select("name, age").Find(&users) -``` - -### Scan - -Scan results into another struct. - -```go -type Result struct { - Name string - Age int -} - -var result Result -db.Table("users").Select("name, age").Where("name = ?", 3).Scan(&result) - -// Raw SQL -db.Raw("SELECT name, age FROM users WHERE name = ?", 3).Scan(&result) -``` - -### Scopes - -```go -func AmountGreaterThan1000(db *gorm.DB) *gorm.DB { - return db.Where("amount > ?", 1000) -} - -func PaidWithCreditCard(db *gorm.DB) *gorm.DB { - return db.Where("pay_mode_sign = ?", "C") -} - -func PaidWithCod(db *gorm.DB) *gorm.DB { - return db.Where("pay_mode_sign = ?", "C") -} - -func OrderStatus(status []string) func (db *gorm.DB) *gorm.DB { - return func (db *gorm.DB) *gorm.DB { - return db.Scopes(AmountGreaterThan1000).Where("status in (?)", status) - } -} - -db.Scopes(AmountGreaterThan1000, PaidWithCreditCard).Find(&orders) -// Find all credit card orders and amount greater than 1000 - -db.Scopes(AmountGreaterThan1000, PaidWithCod).Find(&orders) -// Find all COD orders and amount greater than 1000 - -db.Scopes(OrderStatus([]string{"paid", "shipped"})).Find(&orders) -// Find all paid, shipped orders -``` - -### Specifying The Table Name - -```go -// Create `deleted_users` table with struct User's definition -db.Table("deleted_users").CreateTable(&User{}) - -var deleted_users []User -db.Table("deleted_users").Find(&deleted_users) -//// SELECT * FROM deleted_users; - -db.Table("deleted_users").Where("name = ?", "jinzhu").Delete() -//// DELETE FROM deleted_users WHERE name = 'jinzhu'; -``` - -## Update - -### Update All Fields - -`Save` will include all fields when perform the Updating SQL, even it is not changed - -```go -db.First(&user) - -user.Name = "jinzhu 2" -user.Age = 100 -db.Save(&user) - -//// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111; -``` - -### Update Changed Fields - -If you only want to update changed Fields, you could use `Update`, `Updates` - -```go -// Update single attribute if it is changed -db.Model(&user).Update("name", "hello") -//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111; - -// Update single attribute with combined conditions -db.Model(&user).Where("active = ?", true).Update("name", "hello") -//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true; - - -// Update multiple attributes with `map`, will only update those changed fields -db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) -//// UPDATE users SET name='hello', age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111; - -// Update multiple attributes with `struct`, will only update those changed & non blank fields -db.Model(&user).Updates(User{Name: "hello", Age: 18}) -//// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111; - -// WARNING when update with struct, GORM will only update those fields that with non blank value -// For below Update, nothing will be updated as "", 0, false are blank values of their types -db.Model(&user).Updates(User{Name: "", Age: 0, Actived: false}) -``` - -### Update Selected Fields - -If you only want to update or ignore some fields when updating, you could use `Select`, `Omit` - -```go -db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) -//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111; - -db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) -//// UPDATE users SET age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111; -``` - -### Update Changed Fields Without Callbacks - -Updating operations above will invoke `BeforeUpdate`, `AfterUpdate`, Update UpdatedAt timestamp, Save Associations callbacks, if you don't call them, you could use `UpdateColumn`, `UpdateColumns` - -```go -// Update single attribute, similar with `Update` -db.Model(&user).UpdateColumn("name", "hello") -//// UPDATE users SET name='hello' WHERE id = 111; - -// Update multiple attributes, similar with `Updates` -db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18}) -//// UPDATE users SET name='hello', age=18 WHERE id = 111; -``` - -### Batch Updates - -Callbacks won't run when do batch updates - -```go -db.Table("users").Where("id IN (?)", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18}) -//// UPDATE users SET name='hello', age=18 WHERE id IN (10, 11); - -// Update with struct only works with none zero values, or use map[string]interface{} -db.Model(User{}).Updates(User{Name: "hello", Age: 18}) -//// UPDATE users SET name='hello', age=18; - -// Get updated records count with `RowsAffected` -db.Model(User{}).Updates(User{Name: "hello", Age: 18}).RowsAffected -``` - -### Update with SQL Expression - -```go -DB.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100)) -//// UPDATE "products" SET "price" = price * '2' + '100', "updated_at" = '2013-11-17 21:34:10' WHERE "id" = '2'; - -DB.Model(&product).Updates(map[string]interface{}{"price": gorm.Expr("price * ? + ?", 2, 100)}) -//// UPDATE "products" SET "price" = price * '2' + '100', "updated_at" = '2013-11-17 21:34:10' WHERE "id" = '2'; - -DB.Model(&product).UpdateColumn("quantity", gorm.Expr("quantity - ?", 1)) -//// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = '2'; - -DB.Model(&product).Where("quantity > 1").UpdateColumn("quantity", gorm.Expr("quantity - ?", 1)) -//// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = '2' AND quantity > 1; -``` - -### Change Updating Values In Callbacks - -If you want to change updating values in callbacks using `BeforeUpdate`, `BeforeSave`, you could use `scope.SetColumn`, for example: - -```go -func (user *User) BeforeSave(scope *gorm.Scope) (err error) { - if pw, err := bcrypt.GenerateFromPassword(user.Password, 0); err == nil { - scope.SetColumn("EncryptedPassword", pw) - } -} -``` - -### Extra Updating option - -```go -// Add extra SQL option for updating SQL -db.Model(&user).Set("gorm:update_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Update("name, "hello") -//// UPDATE users SET name='hello', updated_at = '2013-11-17 21:34:10' WHERE id=111 OPTION (OPTIMIZE FOR UNKNOWN); -``` - -## Delete - -```go -// Delete an existing record -db.Delete(&email) -//// DELETE from emails where id=10; - -// Add extra SQL option for deleting SQL -db.Set("gorm:delete_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Delete(&email) -//// DELETE from emails where id=10 OPTION (OPTIMIZE FOR UNKNOWN); -``` - -### Batch Delete - -```go -db.Where("email LIKE ?", "%jinzhu%").Delete(Email{}) -//// DELETE from emails where email LIKE "%jinhu%"; -``` - -### Soft Delete - -If struct has `DeletedAt` field, it will get soft delete ability automatically! -Then it won't be deleted from database permanently when call `Delete`. - -```go -db.Delete(&user) -//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111; - -// Batch Delete -db.Where("age = ?", 20).Delete(&User{}) -//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20; - -// Soft deleted records will be ignored when query them -db.Where("age = 20").Find(&user) -//// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL; - -// Find soft deleted records with Unscoped -db.Unscoped().Where("age = 20").Find(&users) -//// SELECT * FROM users WHERE age = 20; - -// Delete record permanently with Unscoped -db.Unscoped().Delete(&order) -//// DELETE FROM orders WHERE id=10; -``` diff --git a/doc/database.md b/doc/database.md deleted file mode 100644 index 98b6077d..00000000 --- a/doc/database.md +++ /dev/null @@ -1,156 +0,0 @@ -# Database - - - -## Connecting to a database - -#### MySQL - -**NOTE** don't forgot params `parseTime` to handle data type `time.Time`, [more support parameters](https://github.com/go-sql-driver/mysql#parameters) - -```go -import ( - "github.com/jinzhu/gorm" - _ "github.com/go-sql-driver/mysql" -) -func main() { - db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local") -} -``` - -#### PostgreSQL - -```go -import ( - "github.com/jinzhu/gorm" - _ "github.com/lib/pq" -) -func main() { - db, err := gorm.Open("postgres", "user=gorm dbname=gorm sslmode=disable") -} -``` - -#### Sqlite3 - -```go -import ( - "github.com/jinzhu/gorm" - _ "github.com/mattn/go-sqlite3" -) -func main() { - db, err := gorm.Open("sqlite3", "/tmp/gorm.db") -} -``` - -#### Write Dialect for unsupported databases - -GORM officially support above databases, for unsupported databaes, you could write a dialect for that. - -Refer: https://github.com/jinzhu/gorm/blob/master/dialect.go - - -## Generic database object *sql.DB - -[*sql.DB](http://golang.org/pkg/database/sql/#DB) - -```go -// Get generic database object *sql.DB to use its functions -db.DB() - -// Connection Pool -db.DB().SetMaxIdleConns(10) -db.DB().SetMaxOpenConns(100) - - // Ping -db.DB().Ping() -``` - -## Migration - - - -### Auto Migration - -Automatically migrate your schema, to keep your schema update to date - -**WARNING** AutoMigrate will ONLY create tables, columns and indexes if doesn't exist, -WON'T change existing column's type or delete unused columns to protect your data - -```go -db.AutoMigrate(&User{}) - -db.AutoMigrate(&User{}, &Product{}, &Order{}) - -// Add table suffix when create tables -db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{}) -``` - -### Has Table - -```go -// Check if model `User`'s table has been created or not -db.HasTable(&User{}) - -// Check table `users` exists or not -db.HasTable("users") -``` - -### Create Table - -```go -db.CreateTable(&User{}) - -db.Set("gorm:table_options", "ENGINE=InnoDB").CreateTable(&User{}) -// will append "ENGINE=InnoDB" to the SQL statement when creating table `users` -``` - -### Drop table - -```go -db.DropTable(&User{}) -``` - -### ModifyColumn - -Change column's type - -```go -// change column description's data type to `text` for model `User`'s table -db.Model(&User{}).ModifyColumn("description", "text") -``` - -### DropColumn - -```go -db.Model(&User{}).DropColumn("description") -``` - -### Add Foreign Key - -```go -// Add foreign key -// 1st param : foreignkey field -// 2nd param : destination table(id) -// 3rd param : ONDELETE -// 4th param : ONUPDATE -db.Model(&User{}).AddForeignKey("city_id", "cities(id)", "RESTRICT", "RESTRICT") -``` - -### Indexes - -```go -// Add index -db.Model(&User{}).AddIndex("idx_user_name", "name") - -// Multiple column index -db.Model(&User{}).AddIndex("idx_user_name_age", "name", "age") - -// Add unique index -db.Model(&User{}).AddUniqueIndex("idx_user_name", "name") - -// Multiple column unique index -db.Model(&User{}).AddUniqueIndex("idx_user_name_age", "name", "age") - -// Remove index -db.Model(&User{}).RemoveIndex("idx_user_name") -``` diff --git a/doc/development.md b/doc/development.md deleted file mode 100644 index 021cc1f9..00000000 --- a/doc/development.md +++ /dev/null @@ -1,70 +0,0 @@ -# Gorm Development - - - -## Architecture - -The most notable component of Gorm is`gorm.DB`, which hold database connection. It could be initialized like this: - - db, err := gorm.Open("postgres", "user=gorm dbname=gorm sslmode=disable") - -Gorm has chainable API, `gorm.DB` is the bridge of chains, it save related information and pass it to the next chain. - -Lets use below code to explain how it works: - - db.Where("name = ?", "jinzhu").Find(&users) - - // equivalent code - newdb := db.Where("name =?", "jinzhu") - newdb.Find(&user) - -`newdb` is `db`'s clone, in addition, it contains search conditions from the `Where` method. -`Find` is a query method, it creates a `Scope` instance, and pass it as argument to query callbacks. - -There are four kinds of callbacks corresponds to sql's CURD: create callbacks, update callbacks, query callbacks, delete callbacks. - -## Callbacks - -### Register a new callback - - func updateCreated(scope *Scope) { - if scope.HasColumn("Created") { - scope.SetColumn("Created", NowFunc()) - } - } - - db.Callback().Create().Register("update_created_at", updateCreated) - // register a callback for Create process - -### Delete an existing callback - - db.Callback().Create().Remove("gorm:create") - // delete callback `gorm:create` from Create callbacks - -### Replace an existing callback - - db.Callback().Create().Replace("gorm:create", newCreateFunction) - // replace callback `gorm:create` with new function `newCreateFunction` for Create process - -### Register callback orders - - db.Callback().Create().Before("gorm:create").Register("update_created_at", updateCreated) - db.Callback().Create().After("gorm:create").Register("update_created_at", updateCreated) - db.Callback().Query().After("gorm:query").Register("my_plugin:after_query", afterQuery) - db.Callback().Delete().After("gorm:delete").Register("my_plugin:after_delete", afterDelete) - db.Callback().Update().Before("gorm:update").Register("my_plugin:before_update", beforeUpdate) - db.Callback().Create().Before("gorm:create").After("gorm:before_create").Register("my_plugin:before_create", beforeCreate) - -### Callback API - -Gorm is powered by callbacks, so you could refer below links to learn how to write callbacks - -[Create callbacks](https://github.com/jinzhu/gorm/blob/master/callback_create.go) - -[Update callbacks](https://github.com/jinzhu/gorm/blob/master/callback_update.go) - -[Query callbacks](https://github.com/jinzhu/gorm/blob/master/callback_query.go) - -[Delete callbacks](https://github.com/jinzhu/gorm/blob/master/callback_delete.go) - -View [https://github.com/jinzhu/gorm/blob/master/scope.go](https://github.com/jinzhu/gorm/blob/master/scope.go) for all available API diff --git a/doc/logger.png b/doc/logger.png deleted file mode 100644 index 8c46588f0ea9b725e5c569c6fbfc5989ca60155b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66982 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 - -## Model Defination - -```go -type User struct { - gorm.Model - Birthday time.Time - Age int - Name string `sql:"size:255"` // Default size for string is 255, you could reset it with this tag - Num int `sql:"AUTO_INCREMENT"` - IgnoreMe int `sql:"-"` // Ignore this field -} -``` - -## Conventions & Overriding Conventions - -### `gorm.Model` struct - -Gorm has defined struct `gorm.Model`, which could be embeded in your models, it will add fields `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt` to your model - -```go -// Model's definition -type Model struct { - ID uint `gorm:"primary_key"` - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt *time.Time -} -``` - -### Table name is the pluralized version of struct name - -```go -type User struct {} // default table name is `users` - -// set User's table name to be `profiles -type (User) TableName() string { - return "profiles" -} - -func (u User) TableName() string { - if u.Role == "admin" { - return "admin_users" - } else { - return "users" - } -} - -// Disable table name's pluralization globally -db.SingularTable(true) // if set this to true, `User`'s default table name will be `user`, table name setted with `TableName` won't be affected -``` - -### Column name is the snake case of field's name - -```go -type User struct { - ID uint // column name will be `id` - Name string // column name will be `name` - Birthday time.Time // column name will be `birthday` - CreatedAt time.Time // column name will be `created_at` -} - -type Animal struct { - AnimalId int64 `gorm:"column:beast_id"` // set column name to `beast_id` - Birthday time.Time `gorm:"column:day_of_the_beast"` // set column name to `day_of_the_beast` - Age int64 `gorm:"column:age_of_the_beast"` // set column name to `age_of_the_beast` -} -``` - -### Field `ID` as primary key - -```go -type User struct { - ID uint // field named `ID` is the default primary key for `User` - Name string -} - -// your could also use tag `primary_key` to set other field as primary key -type Animal struct { - AnimalId int64 `gorm:"primary_key"` // set AnimalId to be primary key - Name string - Age int64 -} -``` - -### Field `CreatedAt` used to store record's created time - -Create records having `CreatedAt` field will set it to current time. - -```go -db.Create(&user) // will set `CreatedAt` to current time - -// To change its value, you could use `Update` -db.Model(&user).Update("CreatedAt", time.Now()) -``` - -### Use `UpdatedAt` used to store record's updated time - -Save records having `UpdatedAt` field will set it to current time. - -```go -db.Save(&user) // will set `UpdatedAt` to current time -db.Model(&user).Update("name", "jinzhu") // will set `UpdatedAt` to current time -``` - -### Use `DeletedAt` to store record's deleted time if field exists - -Delete records having `DeletedAt` field, it won't delete the record from database, but will set field `DeletedAt`'s value to current time. From 8b291cc32b3c184f438e8531425af929a8630108 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Fri, 4 Mar 2016 08:59:33 +0800 Subject: [PATCH 68/83] Fix format log, close #752 --- logger.go | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/logger.go b/logger.go index fcd0b57c..96c417cc 100644 --- a/logger.go +++ b/logger.go @@ -38,29 +38,40 @@ func (logger Logger) Print(values ...interface{}) { // duration messages = append(messages, fmt.Sprintf(" \033[36;1m[%.2fms]\033[0m ", float64(values[2].(time.Duration).Nanoseconds()/1e4)/100.0)) // sql - var formatedValues []interface{} + var sql string + var formattedValues []string + for _, value := range values[4].([]interface{}) { indirectValue := reflect.Indirect(reflect.ValueOf(value)) if indirectValue.IsValid() { value = indirectValue.Interface() if t, ok := value.(time.Time); ok { - formatedValues = append(formatedValues, fmt.Sprintf("'%v'", t.Format(time.RFC3339))) + formattedValues = append(formattedValues, fmt.Sprintf("'%v'", t.Format(time.RFC3339))) } else if b, ok := value.([]byte); ok { - formatedValues = append(formatedValues, fmt.Sprintf("'%v'", string(b))) + formattedValues = append(formattedValues, fmt.Sprintf("'%v'", string(b))) } else if r, ok := value.(driver.Valuer); ok { if value, err := r.Value(); err == nil && value != nil { - formatedValues = append(formatedValues, fmt.Sprintf("'%v'", value)) + formattedValues = append(formattedValues, fmt.Sprintf("'%v'", value)) } else { - formatedValues = append(formatedValues, "NULL") + formattedValues = append(formattedValues, "NULL") } } else { - formatedValues = append(formatedValues, fmt.Sprintf("'%v'", value)) + formattedValues = append(formattedValues, fmt.Sprintf("'%v'", value)) } } else { - formatedValues = append(formatedValues, fmt.Sprintf("'%v'", value)) + formattedValues = append(formattedValues, fmt.Sprintf("'%v'", value)) } } - messages = append(messages, fmt.Sprintf(sqlRegexp.ReplaceAllString(values[3].(string), "%v"), formatedValues...)) + + var formattedValuesLength = len(formattedValues) + for index, value := range sqlRegexp.Split(values[3].(string), -1) { + sql += value + if index < formattedValuesLength { + sql += formattedValues[index] + } + } + + messages = append(messages, sql) } else { messages = append(messages, "\033[31;1m") messages = append(messages, values[2:]...) From e0cca89ce384fc8706e584283cab6d3022e990f6 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Fri, 4 Mar 2016 09:12:01 +0800 Subject: [PATCH 69/83] Handle binary in log, close #733 --- logger.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/logger.go b/logger.go index 96c417cc..6b948804 100644 --- a/logger.go +++ b/logger.go @@ -8,6 +8,7 @@ import ( "reflect" "regexp" "time" + "unicode" ) type logger interface { @@ -48,7 +49,11 @@ func (logger Logger) Print(values ...interface{}) { if t, ok := value.(time.Time); ok { formattedValues = append(formattedValues, fmt.Sprintf("'%v'", t.Format(time.RFC3339))) } else if b, ok := value.([]byte); ok { - formattedValues = append(formattedValues, fmt.Sprintf("'%v'", string(b))) + if str := string(b); isPrintable(str) { + formattedValues = append(formattedValues, fmt.Sprintf("'%v'", str)) + } else { + formattedValues = append(formattedValues, "''") + } } else if r, ok := value.(driver.Valuer); ok { if value, err := r.Value(); err == nil && value != nil { formattedValues = append(formattedValues, fmt.Sprintf("'%v'", value)) @@ -80,3 +85,12 @@ func (logger Logger) Print(values ...interface{}) { logger.Println(messages...) } } + +func isPrintable(s string) bool { + for _, r := range s { + if !unicode.IsPrint(r) { + return false + } + } + return true +} From c811590d4e06e5991a50222dd5628ffb027c71bf Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sat, 5 Mar 2016 18:54:59 +0800 Subject: [PATCH 70/83] Add dialects for supported databases for easier to use --- CHANGELOG.md | 4 ++- dialect_postgres.go | 51 +--------------------------------- dialects/mssql/mssql.go | 3 ++ dialects/mysql/mysql.go | 3 ++ dialects/postgres/postgres.go | 52 +++++++++++++++++++++++++++++++++++ dialects/sqlite/sqlite.go | 3 ++ main_test.go | 21 +++++++------- 7 files changed, 75 insertions(+), 62 deletions(-) create mode 100644 dialects/mssql/mssql.go create mode 100644 dialects/mysql/mysql.go create mode 100644 dialects/postgres/postgres.go create mode 100644 dialects/sqlite/sqlite.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 528fe1b7..949ad386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ #### Breaking Changes -* **`gorm.Open` return type `*gorm.DB` instead of `gorm.DB`** +* **`gorm.Open` return `*gorm.DB` instead of `gorm.DB`** * **Updating will only update changed fields** @@ -52,3 +52,5 @@ So field `HTTP`'s db name will be `http` not `h_t_t_p`, but some other initialisms like `SKU` that not in golint, it's db name will be `s_k_u`, this release fixed this, any upper case initialisms should be converted correctly. If your applications using some upper case initialisms which doesn't exist in [golint](https://github.com/golang/lint/blob/master/lint.go#L702), you need to overwrite generated column name with tag, like `sql:"column:s_k_u"`, or alert your database's column name according to new logic + +* **Builtin `Hstore` struct for postgres has been moved to `github.com/jinzhu/gorm/dialects/postgres`** diff --git a/dialect_postgres.go b/dialect_postgres.go index 3c18acc2..3d188a65 100644 --- a/dialect_postgres.go +++ b/dialect_postgres.go @@ -1,14 +1,10 @@ package gorm import ( - "database/sql" - "database/sql/driver" "fmt" "reflect" "strings" "time" - - "github.com/lib/pq/hstore" ) type postgres struct { @@ -55,7 +51,7 @@ func (postgres) DataTypeOf(field *StructField) string { sqlType = "timestamp with time zone" } case reflect.Map: - if dataValue.Type() == hstoreType { + if dataValue.Type().Name() == "Hstore" { sqlType = "hstore" } default: @@ -108,51 +104,6 @@ func (postgres) SupportLastInsertId() bool { return false } -var hstoreType = reflect.TypeOf(Hstore{}) - -type Hstore map[string]*string - -func (h Hstore) Value() (driver.Value, error) { - hstore := hstore.Hstore{Map: map[string]sql.NullString{}} - if len(h) == 0 { - return nil, nil - } - - for key, value := range h { - var s sql.NullString - if value != nil { - s.String = *value - s.Valid = true - } - hstore.Map[key] = s - } - return hstore.Value() -} - -func (h *Hstore) Scan(value interface{}) error { - hstore := hstore.Hstore{} - - if err := hstore.Scan(value); err != nil { - return err - } - - if len(hstore.Map) == 0 { - return nil - } - - *h = Hstore{} - for k := range hstore.Map { - if hstore.Map[k].Valid { - s := hstore.Map[k].String - (*h)[k] = &s - } else { - (*h)[k] = nil - } - } - - return nil -} - func isByteArrayOrSlice(value reflect.Value) bool { return (value.Kind() == reflect.Array || value.Kind() == reflect.Slice) && value.Type().Elem() == reflect.TypeOf(uint8(0)) } diff --git a/dialects/mssql/mssql.go b/dialects/mssql/mssql.go new file mode 100644 index 00000000..26bb38eb --- /dev/null +++ b/dialects/mssql/mssql.go @@ -0,0 +1,3 @@ +package mssql + +import _ "github.com/denisenkom/go-mssqldb" diff --git a/dialects/mysql/mysql.go b/dialects/mysql/mysql.go new file mode 100644 index 00000000..9deba48a --- /dev/null +++ b/dialects/mysql/mysql.go @@ -0,0 +1,3 @@ +package mysql + +import _ "github.com/go-sql-driver/mysql" diff --git a/dialects/postgres/postgres.go b/dialects/postgres/postgres.go new file mode 100644 index 00000000..37881090 --- /dev/null +++ b/dialects/postgres/postgres.go @@ -0,0 +1,52 @@ +package postgres + +import ( + "database/sql" + "database/sql/driver" + + _ "github.com/lib/pq" + "github.com/lib/pq/hstore" +) + +type Hstore map[string]*string + +func (h Hstore) Value() (driver.Value, error) { + hstore := hstore.Hstore{Map: map[string]sql.NullString{}} + if len(h) == 0 { + return nil, nil + } + + for key, value := range h { + var s sql.NullString + if value != nil { + s.String = *value + s.Valid = true + } + hstore.Map[key] = s + } + return hstore.Value() +} + +func (h *Hstore) Scan(value interface{}) error { + hstore := hstore.Hstore{} + + if err := hstore.Scan(value); err != nil { + return err + } + + if len(hstore.Map) == 0 { + return nil + } + + *h = Hstore{} + for k := range hstore.Map { + if hstore.Map[k].Valid { + s := hstore.Map[k].String + (*h)[k] = &s + } else { + (*h)[k] = nil + } + } + + return nil +} diff --git a/dialects/sqlite/sqlite.go b/dialects/sqlite/sqlite.go new file mode 100644 index 00000000..069ad3a9 --- /dev/null +++ b/dialects/sqlite/sqlite.go @@ -0,0 +1,3 @@ +package sqlite + +import _ "github.com/mattn/go-sqlite3" diff --git a/main_test.go b/main_test.go index dff91828..c732e7fc 100644 --- a/main_test.go +++ b/main_test.go @@ -4,20 +4,19 @@ import ( "database/sql" "database/sql/driver" "fmt" + "os" "reflect" "strconv" - - _ "github.com/denisenkom/go-mssqldb" - testdb "github.com/erikstmartin/go-testdb" - _ "github.com/go-sql-driver/mysql" - "github.com/jinzhu/gorm" - "github.com/jinzhu/now" - _ "github.com/lib/pq" - _ "github.com/mattn/go-sqlite3" - - "os" "testing" "time" + + "github.com/erikstmartin/go-testdb" + "github.com/jinzhu/gorm" + _ "github.com/jinzhu/gorm/dialects/mssql" + _ "github.com/jinzhu/gorm/dialects/mysql" + "github.com/jinzhu/gorm/dialects/postgres" + _ "github.com/jinzhu/gorm/dialects/sqlite" + "github.com/jinzhu/now" ) var ( @@ -624,7 +623,7 @@ func TestTimeWithZone(t *testing.T) { func TestHstore(t *testing.T) { type Details struct { Id int64 - Bulk gorm.Hstore + Bulk postgres.Hstore } if dialect := os.Getenv("GORM_DIALECT"); dialect != "postgres" { From 2522f03c1fffd3e60fbf742b589f4bfbaf6d5295 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sat, 5 Mar 2016 19:22:29 +0800 Subject: [PATCH 71/83] Set identity insert on after create transaction, close #841 --- callback.go | 2 +- callback_create.go | 18 +++++++++--------- callback_delete.go | 10 +++++----- callback_query.go | 6 +++--- callback_update.go | 18 +++++++++--------- dialects/mssql/mssql.go | 16 +++++++++++++++- main.go | 2 +- 7 files changed, 43 insertions(+), 29 deletions(-) diff --git a/callback.go b/callback.go index d2f0cf04..a17fb2aa 100644 --- a/callback.go +++ b/callback.go @@ -5,7 +5,7 @@ import ( ) // defaultCallback hold default callbacks defined by gorm -var defaultCallback = &Callback{} +var DefaultCallback = &Callback{} // Callback contains callbacks that used when CURD objects // Field `creates` hold callbacks will be call when creating object diff --git a/callback_create.go b/callback_create.go index 6316f9ee..314f505a 100644 --- a/callback_create.go +++ b/callback_create.go @@ -7,15 +7,15 @@ import ( // Define callbacks for creating func init() { - defaultCallback.Create().Register("gorm:begin_transaction", beginTransactionCallback) - defaultCallback.Create().Register("gorm:before_create", beforeCreateCallback) - defaultCallback.Create().Register("gorm:save_before_associations", saveBeforeAssociationsCallback) - defaultCallback.Create().Register("gorm:update_time_stamp", updateTimeStampForCreateCallback) - defaultCallback.Create().Register("gorm:create", createCallback) - defaultCallback.Create().Register("gorm:force_reload_after_create", forceReloadAfterCreateCallback) - defaultCallback.Create().Register("gorm:save_after_associations", saveAfterAssociationsCallback) - defaultCallback.Create().Register("gorm:after_create", afterCreateCallback) - defaultCallback.Create().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback) + DefaultCallback.Create().Register("gorm:begin_transaction", beginTransactionCallback) + DefaultCallback.Create().Register("gorm:before_create", beforeCreateCallback) + DefaultCallback.Create().Register("gorm:save_before_associations", saveBeforeAssociationsCallback) + DefaultCallback.Create().Register("gorm:update_time_stamp", updateTimeStampForCreateCallback) + DefaultCallback.Create().Register("gorm:create", createCallback) + DefaultCallback.Create().Register("gorm:force_reload_after_create", forceReloadAfterCreateCallback) + DefaultCallback.Create().Register("gorm:save_after_associations", saveAfterAssociationsCallback) + DefaultCallback.Create().Register("gorm:after_create", afterCreateCallback) + DefaultCallback.Create().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback) } // beforeCreateCallback will invoke `BeforeSave`, `BeforeCreate` method before creating diff --git a/callback_delete.go b/callback_delete.go index 9db0666c..c8ffcc82 100644 --- a/callback_delete.go +++ b/callback_delete.go @@ -4,11 +4,11 @@ import "fmt" // Define callbacks for deleting func init() { - defaultCallback.Delete().Register("gorm:begin_transaction", beginTransactionCallback) - defaultCallback.Delete().Register("gorm:before_delete", beforeDeleteCallback) - defaultCallback.Delete().Register("gorm:delete", deleteCallback) - defaultCallback.Delete().Register("gorm:after_delete", afterDeleteCallback) - defaultCallback.Delete().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback) + DefaultCallback.Delete().Register("gorm:begin_transaction", beginTransactionCallback) + DefaultCallback.Delete().Register("gorm:before_delete", beforeDeleteCallback) + DefaultCallback.Delete().Register("gorm:delete", deleteCallback) + DefaultCallback.Delete().Register("gorm:after_delete", afterDeleteCallback) + DefaultCallback.Delete().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback) } // beforeDeleteCallback will invoke `BeforeDelete` method before deleting diff --git a/callback_query.go b/callback_query.go index 11f8b476..aa643557 100644 --- a/callback_query.go +++ b/callback_query.go @@ -8,9 +8,9 @@ import ( // Define callbacks for querying func init() { - defaultCallback.Query().Register("gorm:query", queryCallback) - defaultCallback.Query().Register("gorm:preload", preloadCallback) - defaultCallback.Query().Register("gorm:after_query", afterQueryCallback) + DefaultCallback.Query().Register("gorm:query", queryCallback) + DefaultCallback.Query().Register("gorm:preload", preloadCallback) + DefaultCallback.Query().Register("gorm:after_query", afterQueryCallback) } // queryCallback used to query data from database diff --git a/callback_update.go b/callback_update.go index b71a47b4..287b927f 100644 --- a/callback_update.go +++ b/callback_update.go @@ -7,15 +7,15 @@ import ( // Define callbacks for updating func init() { - defaultCallback.Update().Register("gorm:assign_updating_attributes", assignUpdatingAttributesCallback) - defaultCallback.Update().Register("gorm:begin_transaction", beginTransactionCallback) - defaultCallback.Update().Register("gorm:before_update", beforeUpdateCallback) - defaultCallback.Update().Register("gorm:save_before_associations", saveBeforeAssociationsCallback) - defaultCallback.Update().Register("gorm:update_time_stamp", updateTimeStampForUpdateCallback) - defaultCallback.Update().Register("gorm:update", updateCallback) - defaultCallback.Update().Register("gorm:save_after_associations", saveAfterAssociationsCallback) - defaultCallback.Update().Register("gorm:after_update", afterUpdateCallback) - defaultCallback.Update().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback) + DefaultCallback.Update().Register("gorm:assign_updating_attributes", assignUpdatingAttributesCallback) + DefaultCallback.Update().Register("gorm:begin_transaction", beginTransactionCallback) + DefaultCallback.Update().Register("gorm:before_update", beforeUpdateCallback) + DefaultCallback.Update().Register("gorm:save_before_associations", saveBeforeAssociationsCallback) + DefaultCallback.Update().Register("gorm:update_time_stamp", updateTimeStampForUpdateCallback) + DefaultCallback.Update().Register("gorm:update", updateCallback) + DefaultCallback.Update().Register("gorm:save_after_associations", saveAfterAssociationsCallback) + DefaultCallback.Update().Register("gorm:after_update", afterUpdateCallback) + DefaultCallback.Update().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback) } // assignUpdatingAttributesCallback assign updating attributes to model diff --git a/dialects/mssql/mssql.go b/dialects/mssql/mssql.go index 26bb38eb..03ad60fb 100644 --- a/dialects/mssql/mssql.go +++ b/dialects/mssql/mssql.go @@ -1,3 +1,17 @@ package mssql -import _ "github.com/denisenkom/go-mssqldb" +import ( + "fmt" + + _ "github.com/denisenkom/go-mssqldb" + "github.com/jinzhu/gorm" +) + +func setIdentityInsert(scope *gorm.Scope) { + scope.NewDB().Exec(fmt.Sprintf("SET IDENTITY_INSERT %v ON", scope.TableName())) +} + +func init() { + gorm.DefaultCallback.Update().After("gorm:begin_transaction").Register("mssql:set_identity_insert", setIdentityInsert) + gorm.DefaultCallback.Create().After("gorm:begin_transaction").Register("mssql:set_identity_insert", setIdentityInsert) +} diff --git a/main.go b/main.go index cfa71b60..46f35d01 100644 --- a/main.go +++ b/main.go @@ -64,7 +64,7 @@ func Open(dialect string, args ...interface{}) (*DB, error) { db = DB{ dialect: newDialect(dialect, dbSql.(*sql.DB)), logger: defaultLogger, - callbacks: defaultCallback, + callbacks: DefaultCallback, source: source, values: map[string]interface{}{}, db: dbSql, From b6a2710a15ee29bbacea10e17c67e63f4fa78608 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sat, 5 Mar 2016 21:24:54 +0800 Subject: [PATCH 72/83] Don't execute SET IDENTITY_INSERT if dialect is not mssql --- dialect.go | 3 +++ dialect_common.go | 4 ++++ dialect_mssql.go | 4 ++++ dialect_mysql.go | 4 ++++ dialect_postgres.go | 4 ++++ dialect_sqlite3.go | 4 ++++ dialects/mssql/mssql.go | 5 +++-- 7 files changed, 26 insertions(+), 2 deletions(-) diff --git a/dialect.go b/dialect.go index cce68789..897e2570 100644 --- a/dialect.go +++ b/dialect.go @@ -10,6 +10,9 @@ import ( // Dialect interface contains behaviors that differ across SQL database type Dialect interface { + // GetName get dialect's name + GetName() string + // SetDB set db for dialect SetDB(db *sql.DB) diff --git a/dialect_common.go b/dialect_common.go index a1c8ff5c..7afc5c14 100644 --- a/dialect_common.go +++ b/dialect_common.go @@ -16,6 +16,10 @@ func init() { RegisterDialect("common", &commonDialect{}) } +func (commonDialect) GetName() string { + return "common" +} + func (s *commonDialect) SetDB(db *sql.DB) { s.db = db } diff --git a/dialect_mssql.go b/dialect_mssql.go index 2ecc27cc..7a59bb30 100644 --- a/dialect_mssql.go +++ b/dialect_mssql.go @@ -15,6 +15,10 @@ func init() { RegisterDialect("mssql", &mssql{}) } +func (mssql) GetName() string { + return "mssql" +} + func (mssql) DataTypeOf(field *StructField) string { var dataValue, sqlType, size, additionalType = ParseFieldStructForDialect(field) diff --git a/dialect_mysql.go b/dialect_mysql.go index 9e530a9a..d98c33a3 100644 --- a/dialect_mysql.go +++ b/dialect_mysql.go @@ -15,6 +15,10 @@ func init() { RegisterDialect("mysql", &mysql{}) } +func (mysql) GetName() string { + return "mysql" +} + func (mysql) Quote(key string) string { return fmt.Sprintf("`%s`", key) } diff --git a/dialect_postgres.go b/dialect_postgres.go index 3d188a65..96627d92 100644 --- a/dialect_postgres.go +++ b/dialect_postgres.go @@ -15,6 +15,10 @@ func init() { RegisterDialect("postgres", &postgres{}) } +func (postgres) GetName() string { + return "postgres" +} + func (postgres) BindVar(i int) string { return fmt.Sprintf("$%v", i) } diff --git a/dialect_sqlite3.go b/dialect_sqlite3.go index 41e45517..56c847b5 100644 --- a/dialect_sqlite3.go +++ b/dialect_sqlite3.go @@ -16,6 +16,10 @@ func init() { RegisterDialect("sqlite3", &sqlite3{}) } +func (sqlite3) GetName() string { + return "sqlite3" +} + // Get Data Type for Sqlite Dialect func (sqlite3) DataTypeOf(field *StructField) string { var dataValue, sqlType, size, additionalType = ParseFieldStructForDialect(field) diff --git a/dialects/mssql/mssql.go b/dialects/mssql/mssql.go index 03ad60fb..d628ef7c 100644 --- a/dialects/mssql/mssql.go +++ b/dialects/mssql/mssql.go @@ -8,10 +8,11 @@ import ( ) func setIdentityInsert(scope *gorm.Scope) { - scope.NewDB().Exec(fmt.Sprintf("SET IDENTITY_INSERT %v ON", scope.TableName())) + if scope.Dialect().GetName() == "mssql" { + scope.NewDB().Exec(fmt.Sprintf("SET IDENTITY_INSERT %v ON", scope.TableName())) + } } func init() { - gorm.DefaultCallback.Update().After("gorm:begin_transaction").Register("mssql:set_identity_insert", setIdentityInsert) gorm.DefaultCallback.Create().After("gorm:begin_transaction").Register("mssql:set_identity_insert", setIdentityInsert) } From 60a859d9666324ac2a038739130d12039aee91e0 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sat, 5 Mar 2016 22:50:49 +0800 Subject: [PATCH 73/83] Add check HasForeignKey method to dialect, also move mssql dialect to a separate repo as it is not well tested, close #832 --- dialect.go | 2 + dialect_common.go | 4 ++ dialect_mssql.go | 115 ---------------------------------- dialect_mysql.go | 6 ++ dialect_postgres.go | 6 ++ dialects/mssql/mssql.go | 132 ++++++++++++++++++++++++++++++++++++++++ scope_private.go | 4 ++ 7 files changed, 154 insertions(+), 115 deletions(-) delete mode 100644 dialect_mssql.go diff --git a/dialect.go b/dialect.go index 897e2570..adaf43af 100644 --- a/dialect.go +++ b/dialect.go @@ -25,6 +25,8 @@ type Dialect interface { // HasIndex check has index or not HasIndex(tableName string, indexName string) bool + // HasForeignKey check has foreign key or not + HasForeignKey(tableName string, foreignKeyName string) bool // RemoveIndex remove index RemoveIndex(tableName string, indexName string) error // HasTable check has table or not diff --git a/dialect_common.go b/dialect_common.go index 7afc5c14..95553c97 100644 --- a/dialect_common.go +++ b/dialect_common.go @@ -95,6 +95,10 @@ func (s commonDialect) RemoveIndex(tableName string, indexName string) error { return err } +func (s commonDialect) HasForeignKey(tableName string, foreignKeyName string) bool { + return false +} + func (s commonDialect) HasTable(tableName string) bool { var count int s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = ? AND table_name = ?", s.currentDatabase(), tableName).Scan(&count) diff --git a/dialect_mssql.go b/dialect_mssql.go deleted file mode 100644 index 7a59bb30..00000000 --- a/dialect_mssql.go +++ /dev/null @@ -1,115 +0,0 @@ -package gorm - -import ( - "fmt" - "reflect" - "strings" - "time" -) - -type mssql struct { - commonDialect -} - -func init() { - RegisterDialect("mssql", &mssql{}) -} - -func (mssql) GetName() string { - return "mssql" -} - -func (mssql) DataTypeOf(field *StructField) string { - var dataValue, sqlType, size, additionalType = ParseFieldStructForDialect(field) - - if sqlType == "" { - switch dataValue.Kind() { - case reflect.Bool: - sqlType = "bit" - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: - if _, ok := field.TagSettings["AUTO_INCREMENT"]; ok || field.IsPrimaryKey { - sqlType = "int IDENTITY(1,1)" - } else { - sqlType = "int" - } - case reflect.Int64, reflect.Uint64: - if _, ok := field.TagSettings["AUTO_INCREMENT"]; ok || field.IsPrimaryKey { - sqlType = "bigint IDENTITY(1,1)" - } else { - sqlType = "bigint" - } - case reflect.Float32, reflect.Float64: - sqlType = "float" - case reflect.String: - if size > 0 && size < 65532 { - sqlType = fmt.Sprintf("nvarchar(%d)", size) - } else { - sqlType = "text" - } - case reflect.Struct: - if _, ok := dataValue.Interface().(time.Time); ok { - sqlType = "datetime2" - } - default: - if _, ok := dataValue.Interface().([]byte); ok { - if size > 0 && size < 65532 { - sqlType = fmt.Sprintf("varchar(%d)", size) - } else { - sqlType = "text" - } - } - } - } - - if sqlType == "" { - panic(fmt.Sprintf("invalid sql type %s (%s) for mssql", dataValue.Type().Name(), dataValue.Kind().String())) - } - - if strings.TrimSpace(additionalType) == "" { - return sqlType - } - return fmt.Sprintf("%v %v", sqlType, additionalType) -} - -func (s mssql) HasIndex(tableName string, indexName string) bool { - var count int - s.db.QueryRow("SELECT count(*) FROM sys.indexes WHERE name=? AND object_id=OBJECT_ID(?)", indexName, tableName).Scan(&count) - return count > 0 -} - -func (s mssql) RemoveIndex(tableName string, indexName string) error { - _, err := s.db.Exec(fmt.Sprintf("DROP INDEX %v ON %v", indexName, s.Quote(tableName))) - return err -} - -func (s mssql) HasTable(tableName string) bool { - var count int - s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.tables WHERE table_name = ? AND table_catalog = ?", tableName, s.currentDatabase()).Scan(&count) - return count > 0 -} - -func (s mssql) HasColumn(tableName string, columnName string) bool { - var count int - s.db.QueryRow("SELECT count(*) FROM information_schema.columns WHERE table_catalog = ? AND table_name = ? AND column_name = ?", s.currentDatabase(), tableName, columnName).Scan(&count) - return count > 0 -} - -func (s mssql) currentDatabase() (name string) { - s.db.QueryRow("SELECT DB_NAME() AS [Current Database]").Scan(&name) - return -} - -func (mssql) LimitAndOffsetSQL(limit, offset int) (sql string) { - if limit > 0 || offset > 0 { - if offset < 0 { - offset = 0 - } - - sql += fmt.Sprintf(" OFFSET %d ROWS", offset) - - if limit >= 0 { - sql += fmt.Sprintf(" FETCH NEXT %d ROWS ONLY", limit) - } - } - return -} diff --git a/dialect_mysql.go b/dialect_mysql.go index d98c33a3..a6946d5c 100644 --- a/dialect_mysql.go +++ b/dialect_mysql.go @@ -97,6 +97,12 @@ func (s mysql) RemoveIndex(tableName string, indexName string) error { return err } +func (s mysql) HasForeignKey(tableName string, foreignKeyName string) bool { + var count int + s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA=? AND TABLE_NAME=? AND CONSTRAINT_NAME=? AND CONSTRAINT_TYPE='FOREIGN KEY'", s.currentDatabase(), foreignKeyName).Scan(&count) + return count > 0 +} + func (s mysql) currentDatabase() (name string) { s.db.QueryRow("SELECT DATABASE()").Scan(&name) return diff --git a/dialect_postgres.go b/dialect_postgres.go index 96627d92..baf76659 100644 --- a/dialect_postgres.go +++ b/dialect_postgres.go @@ -83,6 +83,12 @@ func (s postgres) HasIndex(tableName string, indexName string) bool { return count > 0 } +func (s postgres) HasForeignKey(tableName string, foreignKeyName string) bool { + var count int + s.db.QueryRow("SELECT count(con.conname) FROM pg_constraint con WHERE $1::regclass::oid = con.conrelid AND con.conname = $2 AND con.contype='f'", s.currentDatabase(), foreignKeyName).Scan(&count) + return count > 0 +} + func (s postgres) HasTable(tableName string) bool { var count int s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.tables WHERE table_name = $1 AND table_type = 'BASE TABLE'", tableName).Scan(&count) diff --git a/dialects/mssql/mssql.go b/dialects/mssql/mssql.go index d628ef7c..657bada9 100644 --- a/dialects/mssql/mssql.go +++ b/dialects/mssql/mssql.go @@ -1,7 +1,11 @@ package mssql import ( + "database/sql" "fmt" + "reflect" + "strings" + "time" _ "github.com/denisenkom/go-mssqldb" "github.com/jinzhu/gorm" @@ -15,4 +19,132 @@ func setIdentityInsert(scope *gorm.Scope) { func init() { gorm.DefaultCallback.Create().After("gorm:begin_transaction").Register("mssql:set_identity_insert", setIdentityInsert) + gorm.RegisterDialect("mssql", &mssql{}) +} + +type mssql struct { + db *sql.DB +} + +func (mssql) GetName() string { + return "mssql" +} + +func (s *mssql) SetDB(db *sql.DB) { + s.db = db +} + +func (mssql) BindVar(i int) string { + return "$$" // ? +} + +func (mssql) Quote(key string) string { + return fmt.Sprintf(`"%s"`, key) +} + +func (mssql) DataTypeOf(field *gorm.StructField) string { + var dataValue, sqlType, size, additionalType = gorm.ParseFieldStructForDialect(field) + + if sqlType == "" { + switch dataValue.Kind() { + case reflect.Bool: + sqlType = "bit" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + if _, ok := field.TagSettings["AUTO_INCREMENT"]; ok || field.IsPrimaryKey { + sqlType = "int IDENTITY(1,1)" + } else { + sqlType = "int" + } + case reflect.Int64, reflect.Uint64: + if _, ok := field.TagSettings["AUTO_INCREMENT"]; ok || field.IsPrimaryKey { + sqlType = "bigint IDENTITY(1,1)" + } else { + sqlType = "bigint" + } + case reflect.Float32, reflect.Float64: + sqlType = "float" + case reflect.String: + if size > 0 && size < 65532 { + sqlType = fmt.Sprintf("nvarchar(%d)", size) + } else { + sqlType = "text" + } + case reflect.Struct: + if _, ok := dataValue.Interface().(time.Time); ok { + sqlType = "datetime2" + } + default: + if _, ok := dataValue.Interface().([]byte); ok { + if size > 0 && size < 65532 { + sqlType = fmt.Sprintf("varchar(%d)", size) + } else { + sqlType = "text" + } + } + } + } + + if sqlType == "" { + panic(fmt.Sprintf("invalid sql type %s (%s) for mssql", dataValue.Type().Name(), dataValue.Kind().String())) + } + + if strings.TrimSpace(additionalType) == "" { + return sqlType + } + return fmt.Sprintf("%v %v", sqlType, additionalType) +} + +func (s mssql) HasIndex(tableName string, indexName string) bool { + var count int + s.db.QueryRow("SELECT count(*) FROM sys.indexes WHERE name=? AND object_id=OBJECT_ID(?)", indexName, tableName).Scan(&count) + return count > 0 +} + +func (s mssql) RemoveIndex(tableName string, indexName string) error { + _, err := s.db.Exec(fmt.Sprintf("DROP INDEX %v ON %v", indexName, s.Quote(tableName))) + return err +} + +func (s mssql) HasForeignKey(tableName string, foreignKeyName string) bool { + return false +} + +func (s mssql) HasTable(tableName string) bool { + var count int + s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.tables WHERE table_name = ? AND table_catalog = ?", tableName, s.currentDatabase()).Scan(&count) + return count > 0 +} + +func (s mssql) HasColumn(tableName string, columnName string) bool { + var count int + s.db.QueryRow("SELECT count(*) FROM information_schema.columns WHERE table_catalog = ? AND table_name = ? AND column_name = ?", s.currentDatabase(), tableName, columnName).Scan(&count) + return count > 0 +} + +func (s mssql) currentDatabase() (name string) { + s.db.QueryRow("SELECT DB_NAME() AS [Current Database]").Scan(&name) + return +} + +func (mssql) LimitAndOffsetSQL(limit, offset int) (sql string) { + if limit > 0 || offset > 0 { + if offset < 0 { + offset = 0 + } + + sql += fmt.Sprintf(" OFFSET %d ROWS", offset) + + if limit >= 0 { + sql += fmt.Sprintf(" FETCH NEXT %d ROWS ONLY", limit) + } + } + return +} + +func (mssql) SelectFromDummyTable() string { + return "" +} + +func (mssql) LastInsertIdReturningSuffix(tableName, columnName string) string { + return "" } diff --git a/scope_private.go b/scope_private.go index d0597c11..b2f504d1 100644 --- a/scope_private.go +++ b/scope_private.go @@ -616,6 +616,10 @@ func (scope *Scope) addIndex(unique bool, indexName string, column ...string) { func (scope *Scope) addForeignKey(field string, dest string, onDelete string, onUpdate string) { var keyName = fmt.Sprintf("%s_%s_%s_foreign", scope.TableName(), field, dest) keyName = regexp.MustCompile("(_*[^a-zA-Z]+_*|_+)").ReplaceAllString(keyName, "_") + + if scope.Dialect().HasForeignKey(scope.TableName(), keyName) { + return + } var query = `ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s ON DELETE %s ON UPDATE %s;` scope.Raw(fmt.Sprintf(query, scope.QuotedTableName(), scope.quoteIfPossible(keyName), scope.quoteIfPossible(field), dest, onDelete, onUpdate)).Exec() } From 37210495bf8f45b01cbdeb8379f49fe427ae8521 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Sun, 6 Mar 2016 09:34:09 +0800 Subject: [PATCH 74/83] Fix check HasColumn for sqlite, close #865 --- dialect_sqlite3.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dialect_sqlite3.go b/dialect_sqlite3.go index 56c847b5..5c262aaf 100644 --- a/dialect_sqlite3.go +++ b/dialect_sqlite3.go @@ -83,7 +83,7 @@ func (s sqlite3) HasTable(tableName string) bool { func (s sqlite3) HasColumn(tableName string, columnName string) bool { var count int - s.db.QueryRow(fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE tbl_name = ? AND (sql LIKE '%%(\"%v\" %%' OR sql LIKE '%%,\"%v\" %%' OR sql LIKE '%%, \"%v\" %%' OR sql LIKE '%%( %v %%' OR sql LIKE '%%, %v %%' OR sql LIKE '%%,%v %%');\n", columnName, columnName, columnName, columnName, columnName, columnName), tableName).Scan(&count) + s.db.QueryRow(fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE tbl_name = ? AND (sql LIKE '%%\"%v\" %%' OR sql LIKE '%%%v %%');\n", columnName, columnName), tableName).Scan(&count) return count > 0 } From 3055bad1e8414fd1675f14217746373a936be524 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 7 Mar 2016 12:15:15 +0800 Subject: [PATCH 75/83] scope.Fields() return slice of *Field --- association.go | 23 ++++++++-------- callback_create.go | 5 ++-- callback_query.go | 2 +- callback_query_preload.go | 4 +-- callback_update.go | 5 ++-- field.go | 47 +++++++++++++++++--------------- field_test.go | 10 ++++--- join_table_handler.go | 12 ++++++--- main.go | 2 +- scope.go | 56 ++++++++++++++++++++++++--------------- scope_private.go | 19 +++---------- 11 files changed, 100 insertions(+), 85 deletions(-) diff --git a/association.go b/association.go index 2df571f5..cd8fd912 100644 --- a/association.go +++ b/association.go @@ -63,9 +63,11 @@ func (association *Association) Replace(values ...interface{}) *Association { var associationForeignFieldNames []string if relationship.Kind == "many_to_many" { // if many to many relations, get association fields name from association foreign keys - associationFields := scope.New(reflect.New(field.Type()).Interface()).Fields() + associationScope := scope.New(reflect.New(field.Type()).Interface()) for _, dbName := range relationship.AssociationForeignFieldNames { - associationForeignFieldNames = append(associationForeignFieldNames, associationFields[dbName].Name) + if field, ok := associationScope.FieldByName(dbName); ok { + associationForeignFieldNames = append(associationForeignFieldNames, field.Name) + } } } else { // If other relations, use primary keys @@ -84,15 +86,12 @@ func (association *Association) Replace(values ...interface{}) *Association { if relationship.Kind == "many_to_many" { // if many to many relations, delete related relations from join table - - // get source fields name from source foreign keys - var ( - sourceFields = scope.Fields() - sourceForeignFieldNames []string - ) + var sourceForeignFieldNames []string for _, dbName := range relationship.ForeignFieldNames { - sourceForeignFieldNames = append(sourceForeignFieldNames, sourceFields[dbName].Name) + if field, ok := scope.FieldByName(dbName); ok { + sourceForeignFieldNames = append(sourceForeignFieldNames, field.Name) + } } if sourcePrimaryKeys := scope.getColumnAsArray(sourceForeignFieldNames, scope.Value); len(sourcePrimaryKeys) > 0 { @@ -147,10 +146,12 @@ func (association *Association) Delete(values ...interface{}) *Association { } // get association's foreign fields name - var associationFields = scope.New(reflect.New(field.Type()).Interface()).Fields() + var associationScope = scope.New(reflect.New(field.Type()).Interface()) var associationForeignFieldNames []string for _, associationDBName := range relationship.AssociationForeignFieldNames { - associationForeignFieldNames = append(associationForeignFieldNames, associationFields[associationDBName].Name) + if field, ok := associationScope.FieldByName(associationDBName); ok { + associationForeignFieldNames = append(associationForeignFieldNames, field.Name) + } } // association value's foreign keys diff --git a/callback_create.go b/callback_create.go index 314f505a..2a0b9b2a 100644 --- a/callback_create.go +++ b/callback_create.go @@ -45,10 +45,9 @@ func createCallback(scope *Scope) { var ( columns, placeholders []string blankColumnsWithDefaultValue []string - fields = scope.Fields() ) - for _, field := range fields { + for _, field := range scope.Fields() { if scope.changeableField(field) { if field.IsNormal { if !field.IsPrimaryKey || !field.IsBlank { @@ -62,7 +61,7 @@ func createCallback(scope *Scope) { } } else if field.Relationship != nil && field.Relationship.Kind == "belongs_to" { for _, foreignKey := range field.Relationship.ForeignDBNames { - if foreignField := fields[foreignKey]; !scope.changeableField(foreignField) { + if foreignField, ok := scope.FieldByName(foreignKey); ok && !scope.changeableField(foreignField) { columns = append(columns, scope.Quote(foreignField.DBName)) placeholders = append(placeholders, scope.AddToVars(foreignField.Field.Interface())) } diff --git a/callback_query.go b/callback_query.go index aa643557..acdf149b 100644 --- a/callback_query.go +++ b/callback_query.go @@ -68,7 +68,7 @@ func queryCallback(scope *Scope) { elem = reflect.New(resultType).Elem() } - scope.scan(rows, columns, scope.New(elem.Addr().Interface()).Fields()) + scope.scan(rows, columns, scope.New(elem.Addr().Interface()).fieldsMap()) if isSlice { if isPtr { diff --git a/callback_query_preload.go b/callback_query_preload.go index e57caad0..1c9bbc84 100644 --- a/callback_query_preload.go +++ b/callback_query_preload.go @@ -255,7 +255,7 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface for rows.Next() { var ( elem = reflect.New(fieldType).Elem() - fields = scope.New(elem.Addr().Interface()).Fields() + fields = scope.New(elem.Addr().Interface()).fieldsMap() ) // register foreign keys in join tables @@ -284,7 +284,7 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface indirectScopeValue = scope.IndirectValue() fieldsSourceMap = map[string]reflect.Value{} foreignFieldNames = []string{} - fields = scope.Fields() + fields = scope.fieldsMap() ) for _, dbName := range relation.ForeignFieldNames { diff --git a/callback_update.go b/callback_update.go index 287b927f..192d8a9e 100644 --- a/callback_update.go +++ b/callback_update.go @@ -60,14 +60,13 @@ func updateCallback(scope *Scope) { sqls = append(sqls, fmt.Sprintf("%v = %v", scope.Quote(column), scope.AddToVars(value))) } } else { - fields := scope.Fields() - for _, field := range fields { + for _, field := range scope.Fields() { if scope.changeableField(field) { if !field.IsPrimaryKey && field.IsNormal { sqls = append(sqls, fmt.Sprintf("%v = %v", scope.Quote(field.DBName), scope.AddToVars(field.Field.Interface()))) } else if relationship := field.Relationship; relationship != nil && relationship.Kind == "belongs_to" { for _, foreignKey := range relationship.ForeignDBNames { - if foreignField := fields[foreignKey]; !scope.changeableField(foreignField) { + if foreignField, ok := scope.FieldByName(foreignKey); ok && !scope.changeableField(foreignField) { sqls = append(sqls, fmt.Sprintf("%v = %v", scope.Quote(foreignField.DBName), scope.AddToVars(foreignField.Field.Interface()))) } diff --git a/field.go b/field.go index 2f0daf77..ff252e1a 100644 --- a/field.go +++ b/field.go @@ -56,29 +56,34 @@ func (field *Field) Set(value interface{}) (err error) { } // Fields get value's fields -func (scope *Scope) Fields() map[string]*Field { - if scope.fields == nil { - var ( - fields = map[string]*Field{} - indirectScopeValue = scope.IndirectValue() - isStruct = indirectScopeValue.Kind() == reflect.Struct - ) +func (scope *Scope) Fields() []*Field { + var ( + fields []*Field + indirectScopeValue = scope.IndirectValue() + isStruct = indirectScopeValue.Kind() == reflect.Struct + ) - for _, structField := range scope.GetModelStruct().StructFields { - if field, ok := fields[structField.DBName]; !ok || field.IsIgnored { - if isStruct { - fieldValue := indirectScopeValue - for _, name := range structField.Names { - fieldValue = reflect.Indirect(fieldValue).FieldByName(name) - } - fields[structField.DBName] = &Field{StructField: structField, Field: fieldValue, IsBlank: isBlank(fieldValue)} - } else { - fields[structField.DBName] = &Field{StructField: structField, IsBlank: true} - } + for _, structField := range scope.GetModelStruct().StructFields { + if isStruct { + fieldValue := indirectScopeValue + for _, name := range structField.Names { + fieldValue = reflect.Indirect(fieldValue).FieldByName(name) } + fields = append(fields, &Field{StructField: structField, Field: fieldValue, IsBlank: isBlank(fieldValue)}) + } else { + fields = append(fields, &Field{StructField: structField, IsBlank: true}) } - - scope.fields = fields } - return scope.fields + + return fields +} + +func (scope *Scope) fieldsMap() map[string]*Field { + var results = map[string]*Field{} + for _, field := range scope.Fields() { + if field.IsNormal { + results[field.DBName] = field + } + } + return results } diff --git a/field_test.go b/field_test.go index 2172b059..30e9a778 100644 --- a/field_test.go +++ b/field_test.go @@ -32,12 +32,16 @@ type CalculateFieldCategory struct { func TestCalculateField(t *testing.T) { var field CalculateField - fields := DB.NewScope(&field).Fields() - if fields["children"].Relationship == nil || fields["category"].Relationship == nil { + var scope = DB.NewScope(&field) + if field, ok := scope.FieldByName("Children"); !ok || field.Relationship == nil { t.Errorf("Should calculate fields correctly for the first time") } - if field, ok := fields["embedded_name"]; !ok { + if field, ok := scope.FieldByName("Category"); !ok || field.Relationship == nil { + t.Errorf("Should calculate fields correctly for the first time") + } + + if field, ok := scope.FieldByName("embedded_name"); !ok { t.Errorf("should find embedded field") } else if _, ok := field.TagSettings["NOT NULL"]; !ok { t.Errorf("should find embedded field's tag settings") diff --git a/join_table_handler.go b/join_table_handler.go index 9e6c027a..6251cd22 100644 --- a/join_table_handler.go +++ b/join_table_handler.go @@ -74,11 +74,15 @@ func (s JoinTableHandler) GetSearchMap(db *DB, sources ...interface{}) map[strin if s.Source.ModelType == modelType { for _, foreignKey := range s.Source.ForeignKeys { - values[foreignKey.DBName] = scope.Fields()[foreignKey.AssociationDBName].Field.Interface() + if field, ok := scope.FieldByName(foreignKey.AssociationDBName); ok { + values[foreignKey.DBName] = field.Field.Interface() + } } } else if s.Destination.ModelType == modelType { for _, foreignKey := range s.Destination.ForeignKeys { - values[foreignKey.DBName] = scope.Fields()[foreignKey.AssociationDBName].Field.Interface() + if field, ok := scope.FieldByName(foreignKey.AssociationDBName); ok { + values[foreignKey.DBName] = field.Field.Interface() + } } } } @@ -151,7 +155,9 @@ func (s JoinTableHandler) JoinWith(handler JoinTableHandlerInterface, db *DB, so for _, foreignKey := range s.Source.ForeignKeys { foreignDBNames = append(foreignDBNames, foreignKey.DBName) - foreignFieldNames = append(foreignFieldNames, scope.Fields()[foreignKey.AssociationDBName].Name) + if field, ok := scope.FieldByName(foreignKey.AssociationDBName); ok { + foreignFieldNames = append(foreignFieldNames, field.Name) + } } foreignFieldValues := scope.getColumnAsArray(foreignFieldNames, scope.Value) diff --git a/main.go b/main.go index 46f35d01..09b6df74 100644 --- a/main.go +++ b/main.go @@ -232,7 +232,7 @@ func (s *DB) ScanRows(rows *sql.Rows, value interface{}) error { ) if clone.AddError(err) == nil { - scope.scan(rows, columns, scope.Fields()) + scope.scan(rows, columns, scope.fieldsMap()) } return clone.Error diff --git a/scope.go b/scope.go index c84a8179..9f4e821d 100644 --- a/scope.go +++ b/scope.go @@ -100,10 +100,11 @@ func (scope *Scope) HasError() bool { return scope.db.Error != nil } -func (scope *Scope) PrimaryFields() []*Field { - var fields = []*Field{} - for _, field := range scope.GetModelStruct().PrimaryFields { - fields = append(fields, scope.Fields()[field.DBName]) +func (scope *Scope) PrimaryFields() (fields []*Field) { + for _, field := range scope.Fields() { + if field.IsPrimaryKey { + fields = append(fields, field) + } } return fields } @@ -111,11 +112,11 @@ func (scope *Scope) PrimaryFields() []*Field { func (scope *Scope) PrimaryField() *Field { if primaryFields := scope.GetModelStruct().PrimaryFields; len(primaryFields) > 0 { if len(primaryFields) > 1 { - if field, ok := scope.Fields()["id"]; ok { + if field, ok := scope.FieldByName("id"); ok { return field } } - return scope.Fields()[primaryFields[0].DBName] + return scope.PrimaryFields()[0] } return nil } @@ -164,20 +165,23 @@ func (scope *Scope) SetColumn(column interface{}, value interface{}) error { updateAttrs[field.DBName] = value return field.Set(value) } else if name, ok := column.(string); ok { - if field, ok := scope.Fields()[name]; ok { - updateAttrs[field.DBName] = value - return field.Set(value) + var ( + dbName = ToDBName(name) + mostMatchedField *Field + ) + for _, field := range scope.Fields() { + if field.DBName == value { + updateAttrs[field.DBName] = value + return field.Set(value) + } + if (field.DBName == dbName) || (field.Name == name && mostMatchedField == nil) { + mostMatchedField = field + } } - dbName := ToDBName(name) - if field, ok := scope.Fields()[dbName]; ok { - updateAttrs[field.DBName] = value - return field.Set(value) - } - - if field, ok := scope.FieldByName(name); ok { - updateAttrs[field.DBName] = value - return field.Set(value) + if mostMatchedField != nil { + updateAttrs[mostMatchedField.DBName] = value + return mostMatchedField.Set(value) } } return errors.New("could not convert column to field") @@ -286,12 +290,20 @@ func (scope *Scope) CombinedConditionSql() string { // FieldByName find gorm.Field with name and db name func (scope *Scope) FieldByName(name string) (field *Field, ok bool) { + var ( + dbName = ToDBName(name) + mostMatchedField *Field + ) + for _, field := range scope.Fields() { if field.Name == name || field.DBName == name { return field, true } + if field.DBName == dbName { + mostMatchedField = field + } } - return nil, false + return mostMatchedField, mostMatchedField != nil } // Raw set sql @@ -390,12 +402,12 @@ func (scope *Scope) OmitAttrs() []string { return scope.Search.omits } -func (scope *Scope) scan(rows *sql.Rows, columns []string, fields map[string]*Field) { +func (scope *Scope) scan(rows *sql.Rows, columns []string, fieldsMap map[string]*Field) { var values = make([]interface{}, len(columns)) var ignored interface{} for index, column := range columns { - if field, ok := fields[column]; ok { + if field, ok := fieldsMap[column]; ok { if field.Field.Kind() == reflect.Ptr { values[index] = field.Field.Addr().Interface() } else { @@ -411,7 +423,7 @@ func (scope *Scope) scan(rows *sql.Rows, columns []string, fields map[string]*Fi scope.Err(rows.Scan(values...)) for index, column := range columns { - if field, ok := fields[column]; ok { + if field, ok := fieldsMap[column]; ok { if field.Field.Kind() != reflect.Ptr { if v := reflect.ValueOf(values[index]).Elem().Elem(); v.IsValid() { field.Field.Set(v) diff --git a/scope_private.go b/scope_private.go index b2f504d1..31db4a0b 100644 --- a/scope_private.go +++ b/scope_private.go @@ -437,21 +437,10 @@ func (scope *Scope) shouldSaveAssociations() bool { func (scope *Scope) related(value interface{}, foreignKeys ...string) *Scope { toScope := scope.db.NewScope(value) - fromFields := scope.Fields() - toFields := toScope.Fields() for _, foreignKey := range append(foreignKeys, toScope.typeName()+"Id", scope.typeName()+"Id") { - var fromField, toField *Field - if field, ok := scope.FieldByName(foreignKey); ok { - fromField = field - } else { - fromField = fromFields[ToDBName(foreignKey)] - } - if field, ok := toScope.FieldByName(foreignKey); ok { - toField = field - } else { - toField = toFields[ToDBName(foreignKey)] - } + fromField, _ := scope.FieldByName(foreignKey) + toField, _ := toScope.FieldByName(foreignKey) if fromField != nil { if relationship := fromField.Relationship; relationship != nil { @@ -515,7 +504,7 @@ func (scope *Scope) createJoinTable(field *StructField) { var sqlTypes, primaryKeys []string for idx, fieldName := range relationship.ForeignFieldNames { - if field, ok := scope.Fields()[fieldName]; ok { + if field, ok := scope.FieldByName(fieldName); ok { foreignKeyStruct := field.clone() foreignKeyStruct.IsPrimaryKey = false foreignKeyStruct.TagSettings["IS_JOINTABLE_FOREIGNKEY"] = "true" @@ -525,7 +514,7 @@ func (scope *Scope) createJoinTable(field *StructField) { } for idx, fieldName := range relationship.AssociationForeignFieldNames { - if field, ok := toScope.Fields()[fieldName]; ok { + if field, ok := toScope.FieldByName(fieldName); ok { foreignKeyStruct := field.clone() foreignKeyStruct.IsPrimaryKey = false foreignKeyStruct.TagSettings["IS_JOINTABLE_FOREIGNKEY"] = "true" From ec110657da71ad209334cea7d3d84bc8d0b3365e Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 7 Mar 2016 14:54:20 +0800 Subject: [PATCH 76/83] Refactor based on golint --- callback.go | 2 +- callback_create.go | 12 +++---- callback_query.go | 8 ++--- dialect.go | 2 +- dialect_common.go | 2 +- dialect_postgres.go | 4 +-- dialects/mssql/mssql.go | 2 +- errors.go | 16 +++++++--- field.go | 2 ++ join_table_handler.go | 26 ++++++++++++--- logger.go | 16 +++++----- main.go | 70 ++++++++++++++++++++++++++--------------- main_test.go | 4 +-- model.go | 1 + model_struct.go | 5 +++ preload_test.go | 6 ++-- scope.go | 33 ++++++++++++------- scope_private.go | 66 +++++++++++++++++++------------------- search.go | 6 ++-- utils.go | 11 +++++++ 20 files changed, 185 insertions(+), 109 deletions(-) diff --git a/callback.go b/callback.go index a17fb2aa..9704e584 100644 --- a/callback.go +++ b/callback.go @@ -4,7 +4,7 @@ import ( "fmt" ) -// defaultCallback hold default callbacks defined by gorm +// DefaultCallback default callbacks defined by gorm var DefaultCallback = &Callback{} // Callback contains callbacks that used when CURD objects diff --git a/callback_create.go b/callback_create.go index 2a0b9b2a..e3cd2f0b 100644 --- a/callback_create.go +++ b/callback_create.go @@ -85,14 +85,14 @@ func createCallback(scope *Scope) { returningColumn = scope.Quote(primaryField.DBName) } - lastInsertIdReturningSuffix := scope.Dialect().LastInsertIdReturningSuffix(quotedTableName, returningColumn) + lastInsertIDReturningSuffix := scope.Dialect().LastInsertIDReturningSuffix(quotedTableName, returningColumn) if len(columns) == 0 { scope.Raw(fmt.Sprintf( "INSERT INTO %v DEFAULT VALUES%v%v", quotedTableName, addExtraSpaceIfExist(extraOption), - addExtraSpaceIfExist(lastInsertIdReturningSuffix), + addExtraSpaceIfExist(lastInsertIDReturningSuffix), )) } else { scope.Raw(fmt.Sprintf( @@ -101,13 +101,13 @@ func createCallback(scope *Scope) { strings.Join(columns, ","), strings.Join(placeholders, ","), addExtraSpaceIfExist(extraOption), - addExtraSpaceIfExist(lastInsertIdReturningSuffix), + addExtraSpaceIfExist(lastInsertIDReturningSuffix), )) } // execute create sql - if lastInsertIdReturningSuffix == "" || primaryField == nil { - if result, err := scope.SqlDB().Exec(scope.Sql, scope.SqlVars...); scope.Err(err) == nil { + if lastInsertIDReturningSuffix == "" || primaryField == nil { + if result, err := scope.SQLDB().Exec(scope.SQL, scope.SQLVars...); scope.Err(err) == nil { // set rows affected count scope.db.RowsAffected, _ = result.RowsAffected() @@ -119,7 +119,7 @@ func createCallback(scope *Scope) { } } } else { - if err := scope.SqlDB().QueryRow(scope.Sql, scope.SqlVars...).Scan(primaryField.Field.Addr().Interface()); scope.Err(err) == nil { + if err := scope.SQLDB().QueryRow(scope.SQL, scope.SQLVars...).Scan(primaryField.Field.Addr().Interface()); scope.Err(err) == nil { scope.db.RowsAffected = 1 } } diff --git a/callback_query.go b/callback_query.go index acdf149b..08678f92 100644 --- a/callback_query.go +++ b/callback_query.go @@ -48,15 +48,15 @@ func queryCallback(scope *Scope) { return } - scope.prepareQuerySql() + scope.prepareQuerySQL() if !scope.HasError() { scope.db.RowsAffected = 0 if str, ok := scope.Get("gorm:query_option"); ok { - scope.Sql += addExtraSpaceIfExist(fmt.Sprint(str)) + scope.SQL += addExtraSpaceIfExist(fmt.Sprint(str)) } - if rows, err := scope.SqlDB().Query(scope.Sql, scope.SqlVars...); scope.Err(err) == nil { + if rows, err := scope.SQLDB().Query(scope.SQL, scope.SQLVars...); scope.Err(err) == nil { defer rows.Close() columns, _ := rows.Columns() @@ -80,7 +80,7 @@ func queryCallback(scope *Scope) { } if scope.db.RowsAffected == 0 && !isSlice { - scope.Err(RecordNotFound) + scope.Err(ErrRecordNotFound) } } } diff --git a/dialect.go b/dialect.go index adaf43af..1d757078 100644 --- a/dialect.go +++ b/dialect.go @@ -39,7 +39,7 @@ type Dialect interface { // SelectFromDummyTable return select values, for most dbs, `SELECT values` just works, mysql needs `SELECT value FROM DUAL` SelectFromDummyTable() string // LastInsertIdReturningSuffix most dbs support LastInsertId, but postgres needs to use `RETURNING` - LastInsertIdReturningSuffix(tableName, columnName string) string + LastInsertIDReturningSuffix(tableName, columnName string) string } var dialectsMap = map[string]Dialect{} diff --git a/dialect_common.go b/dialect_common.go index 95553c97..f009271b 100644 --- a/dialect_common.go +++ b/dialect_common.go @@ -132,6 +132,6 @@ func (commonDialect) SelectFromDummyTable() string { return "" } -func (commonDialect) LastInsertIdReturningSuffix(tableName, columnName string) string { +func (commonDialect) LastInsertIDReturningSuffix(tableName, columnName string) string { return "" } diff --git a/dialect_postgres.go b/dialect_postgres.go index baf76659..18cbe028 100644 --- a/dialect_postgres.go +++ b/dialect_postgres.go @@ -106,11 +106,11 @@ func (s postgres) currentDatabase() (name string) { return } -func (s postgres) LastInsertIdReturningSuffix(tableName, key string) string { +func (s postgres) LastInsertIDReturningSuffix(tableName, key string) string { return fmt.Sprintf("RETURNING %v.%v", tableName, key) } -func (postgres) SupportLastInsertId() bool { +func (postgres) SupportLastInsertID() bool { return false } diff --git a/dialects/mssql/mssql.go b/dialects/mssql/mssql.go index 657bada9..5b994f9d 100644 --- a/dialects/mssql/mssql.go +++ b/dialects/mssql/mssql.go @@ -145,6 +145,6 @@ func (mssql) SelectFromDummyTable() string { return "" } -func (mssql) LastInsertIdReturningSuffix(tableName, columnName string) string { +func (mssql) LastInsertIDReturningSuffix(tableName, columnName string) string { return "" } diff --git a/errors.go b/errors.go index c59dd968..32b7f220 100644 --- a/errors.go +++ b/errors.go @@ -6,24 +6,31 @@ import ( ) var ( - RecordNotFound = errors.New("record not found") - InvalidSql = errors.New("invalid sql") - NoValidTransaction = errors.New("no valid transaction") - CantStartTransaction = errors.New("can't start transaction") + // ErrRecordNotFound record not found, happens when you are looking up with a struct, and haven't find any matched data + ErrRecordNotFound = errors.New("record not found") + // ErrInvalidSQL invalid SQL, happens when you passed invalid SQL + ErrInvalidSQL = errors.New("invalid SQL") + // ErrInvalidTransaction invalid transaction when you are trying to `Commit` or `Rollback` + ErrInvalidTransaction = errors.New("no valid transaction") + // ErrCantStartTransaction can't start transaction when you are trying to start one with `Begin` + ErrCantStartTransaction = errors.New("can't start transaction") ) type errorsInterface interface { GetErrors() []error } +// Errors contains all happened errors type Errors struct { errors []error } +// GetErrors get all happened errors func (errs Errors) GetErrors() []error { return errs.errors } +// Add add an error func (errs *Errors) Add(err error) { if errors, ok := err.(errorsInterface); ok { for _, err := range errors.GetErrors() { @@ -39,6 +46,7 @@ func (errs *Errors) Add(err error) { } } +// Error format happened errors func (errs Errors) Error() string { var errors = []string{} for _, e := range errs.errors { diff --git a/field.go b/field.go index ff252e1a..cadc1a72 100644 --- a/field.go +++ b/field.go @@ -7,12 +7,14 @@ import ( "reflect" ) +// Field model field definition type Field struct { *StructField IsBlank bool Field reflect.Value } +// Set set a value to the field func (field *Field) Set(value interface{}) (err error) { if !field.Field.IsValid() { return errors.New("field value not valid") diff --git a/join_table_handler.go b/join_table_handler.go index 6251cd22..18c12a85 100644 --- a/join_table_handler.go +++ b/join_table_handler.go @@ -7,40 +7,54 @@ import ( "strings" ) +// JoinTableHandlerInterface is an interface for how to handle many2many relations type JoinTableHandlerInterface interface { + // initialize join table handler Setup(relationship *Relationship, tableName string, source reflect.Type, destination reflect.Type) + // Table return join table's table name Table(db *DB) string + // Add create relationship in join table for source and destination Add(handler JoinTableHandlerInterface, db *DB, source interface{}, destination interface{}) error + // Delete delete relationship in join table for sources Delete(handler JoinTableHandlerInterface, db *DB, sources ...interface{}) error + // JoinWith query with `Join` conditions JoinWith(handler JoinTableHandlerInterface, db *DB, source interface{}) *DB + // SourceForeignKeys return source foreign keys SourceForeignKeys() []JoinTableForeignKey + // DestinationForeignKeys return destination foreign keys DestinationForeignKeys() []JoinTableForeignKey } +// JoinTableForeignKey join table foreign key struct type JoinTableForeignKey struct { DBName string AssociationDBName string } +// JoinTableSource is a struct that contains model type and foreign keys type JoinTableSource struct { ModelType reflect.Type ForeignKeys []JoinTableForeignKey } +// JoinTableHandler default join table handler type JoinTableHandler struct { TableName string `sql:"-"` Source JoinTableSource `sql:"-"` Destination JoinTableSource `sql:"-"` } +// SourceForeignKeys return source foreign keys func (s *JoinTableHandler) SourceForeignKeys() []JoinTableForeignKey { return s.Source.ForeignKeys } +// DestinationForeignKeys return destination foreign keys func (s *JoinTableHandler) DestinationForeignKeys() []JoinTableForeignKey { return s.Destination.ForeignKeys } +// Setup initialize a default join table handler func (s *JoinTableHandler) Setup(relationship *Relationship, tableName string, source reflect.Type, destination reflect.Type) { s.TableName = tableName @@ -61,11 +75,12 @@ func (s *JoinTableHandler) Setup(relationship *Relationship, tableName string, s } } +// Table return join table's table name func (s JoinTableHandler) Table(db *DB) string { return s.TableName } -func (s JoinTableHandler) GetSearchMap(db *DB, sources ...interface{}) map[string]interface{} { +func (s JoinTableHandler) getSearchMap(db *DB, sources ...interface{}) map[string]interface{} { values := map[string]interface{}{} for _, source := range sources { @@ -89,9 +104,10 @@ func (s JoinTableHandler) GetSearchMap(db *DB, sources ...interface{}) map[strin return values } -func (s JoinTableHandler) Add(handler JoinTableHandlerInterface, db *DB, source1 interface{}, source2 interface{}) error { +// Add create relationship in join table for source and destination +func (s JoinTableHandler) Add(handler JoinTableHandlerInterface, db *DB, source interface{}, destination interface{}) error { scope := db.NewScope("") - searchMap := s.GetSearchMap(db, source1, source2) + searchMap := s.getSearchMap(db, source, destination) var assignColumns, binVars, conditions []string var values []interface{} @@ -120,6 +136,7 @@ func (s JoinTableHandler) Add(handler JoinTableHandlerInterface, db *DB, source1 return db.Exec(sql, values...).Error } +// Delete delete relationship in join table for sources func (s JoinTableHandler) Delete(handler JoinTableHandlerInterface, db *DB, sources ...interface{}) error { var ( scope = db.NewScope(nil) @@ -127,7 +144,7 @@ func (s JoinTableHandler) Delete(handler JoinTableHandlerInterface, db *DB, sour values []interface{} ) - for key, value := range s.GetSearchMap(db, sources...) { + for key, value := range s.getSearchMap(db, sources...) { conditions = append(conditions, fmt.Sprintf("%v = ?", scope.Quote(key))) values = append(values, value) } @@ -135,6 +152,7 @@ func (s JoinTableHandler) Delete(handler JoinTableHandlerInterface, db *DB, sour return db.Table(handler.Table(db)).Where(strings.Join(conditions, " AND "), values...).Delete("").Error } +// JoinWith query with `Join` conditions func (s JoinTableHandler) JoinWith(handler JoinTableHandlerInterface, db *DB, source interface{}) *DB { var ( scope = db.NewScope(source) diff --git a/logger.go b/logger.go index 6b948804..f4c79ead 100644 --- a/logger.go +++ b/logger.go @@ -11,23 +11,25 @@ import ( "unicode" ) +var ( + defaultLogger = Logger{log.New(os.Stdout, "\r\n", 0)} + sqlRegexp = regexp.MustCompile(`(\$\d+)|\?`) +) + type logger interface { Print(v ...interface{}) } -type LogWriter interface { +type logWriter interface { Println(v ...interface{}) } +// Logger default logger type Logger struct { - LogWriter + logWriter } -var defaultLogger = Logger{log.New(os.Stdout, "\r\n", 0)} - -// Format log -var sqlRegexp = regexp.MustCompile(`(\$\d+)|\?`) - +// Print format & print log func (logger Logger) Print(values ...interface{}) { if len(values) > 1 { level := values[0] diff --git a/main.go b/main.go index 09b6df74..b4b8a169 100644 --- a/main.go +++ b/main.go @@ -6,19 +6,9 @@ import ( "fmt" "reflect" "strings" - "time" ) -// NowFunc returns current time, this function is exported in order to be able -// to give the flexibility to the developer to customize it according to their -// needs -// -// e.g: return time.Now().UTC() -// -var NowFunc = func() time.Time { - return time.Now() -} - +// DB contains information for current db connection type DB struct { Value interface{} Error error @@ -36,6 +26,14 @@ type DB struct { joinTableHandlers map[string]JoinTableHandler } +// Open open a new db connection, need to import driver first, for example: +// +// import _ "github.com/go-sql-driver/mysql" +// func main() { +// db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local") +// } +// GORM has wrapped some drivers, for easier to remember its name, so you could import the mysql driver with +// import _ "github.com/jinzhu/gorm/dialects/mysql" func Open(dialect string, args ...interface{}) (*DB, error) { var db DB var err error @@ -44,7 +42,7 @@ func Open(dialect string, args ...interface{}) (*DB, error) { err = errors.New("invalid database source") } else { var source string - var dbSql sqlCommon + var dbSQL sqlCommon switch value := args[0].(type) { case string: @@ -55,19 +53,19 @@ func Open(dialect string, args ...interface{}) (*DB, error) { driver = value source = args[1].(string) } - dbSql, err = sql.Open(driver, source) + dbSQL, err = sql.Open(driver, source) case sqlCommon: source = reflect.Indirect(reflect.ValueOf(value)).FieldByName("dsn").String() - dbSql = value + dbSQL = value } db = DB{ - dialect: newDialect(dialect, dbSql.(*sql.DB)), + dialect: newDialect(dialect, dbSQL.(*sql.DB)), logger: defaultLogger, callbacks: DefaultCallback, source: source, values: map[string]interface{}{}, - db: dbSql, + db: dbSQL, } db.parent = &db @@ -79,14 +77,17 @@ func Open(dialect string, args ...interface{}) (*DB, error) { return &db, err } +// Close close current db connection func (s *DB) Close() error { return s.parent.db.(*sql.DB).Close() } +// DB get `*sql.DB` from current connection func (s *DB) DB() *sql.DB { return s.db.(*sql.DB) } +// New initialize a new db connection without any search conditions func (s *DB) New() *DB { clone := s.clone() clone.search = nil @@ -94,29 +95,34 @@ func (s *DB) New() *DB { return clone } -// NewScope create scope for callbacks, including DB's search information +// NewScope create a scope for current operation func (s *DB) NewScope(value interface{}) *Scope { dbClone := s.clone() dbClone.Value = value return &Scope{db: dbClone, Search: dbClone.search.clone(), Value: value} } -// CommonDB Return the underlying sql.DB or sql.Tx instance. +// CommonDB return the underlying sql.DB or sql.Tx instance. // Use of this method is discouraged. It's mainly intended to allow // coexistence with legacy non-GORM code. func (s *DB) CommonDB() sqlCommon { return s.db } +// Callback return Callbacks container, you could add/remove/change callbacks with it +// db.Callback().Create().Register("update_created_at", updateCreated) +// Refer: https://jinzhu.github.io/gorm/development.html#callbacks for more func (s *DB) Callback() *Callback { s.parent.callbacks = s.parent.callbacks.clone() return s.parent.callbacks } -func (s *DB) SetLogger(l logger) { - s.logger = l +// SetLogger replace default logger +func (s *DB) SetLogger(log logger) { + s.logger = log } +// LogMode set log mode, `true` for detailed logs, `false` for no log, default, will only print error logs func (s *DB) LogMode(enable bool) *DB { if enable { s.logMode = 2 @@ -126,51 +132,65 @@ func (s *DB) LogMode(enable bool) *DB { return s } +// SingularTable use singular table by default func (s *DB) SingularTable(enable bool) { modelStructsMap = newModelStructsMap() s.parent.singularTable = enable } +// Where return a new relation, accepts use `map`, `struct` or `string` as conditions, refer http://jinzhu.github.io/gorm/curd.html#query func (s *DB) Where(query interface{}, args ...interface{}) *DB { return s.clone().search.Where(query, args...).db } +// Or match before conditions or this one, similar to `Where` func (s *DB) Or(query interface{}, args ...interface{}) *DB { return s.clone().search.Or(query, args...).db } +// Not don't match current conditions, similar to `Where` func (s *DB) Not(query interface{}, args ...interface{}) *DB { return s.clone().search.Not(query, args...).db } +// Limit specify the number of records to be retrieved func (s *DB) Limit(limit int) *DB { return s.clone().search.Limit(limit).db } +// Offset specify the number of records to skip before starting to return the records func (s *DB) Offset(offset int) *DB { return s.clone().search.Offset(offset).db } +// Order specify order when retrieve records from database, pass `true` as the second argument to overwrite `Order` conditions func (s *DB) Order(value string, reorder ...bool) *DB { return s.clone().search.Order(value, reorder...).db } +// Select When querying, specify fields that you want to retrieve from database, by default, will select all fields; +// When creating/updating, specify fields that you want to save to database func (s *DB) Select(query interface{}, args ...interface{}) *DB { return s.clone().search.Select(query, args...).db } +// Omit specify fields that you want to ignore when save to database when creating/updating func (s *DB) Omit(columns ...string) *DB { return s.clone().search.Omit(columns...).db } +// Group specify the group method on the find func (s *DB) Group(query string) *DB { return s.clone().search.Group(query).db } +// Having specify HAVING conditions for GROUP BY func (s *DB) Having(query string, values ...interface{}) *DB { return s.clone().search.Having(query, values...).db } +// Joins specify Joins conditions +// db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Find(&user) func (s *DB) Joins(query string, args ...interface{}) *DB { return s.clone().search.Joins(query, args...).db } @@ -352,7 +372,7 @@ func (s *DB) Begin() *DB { c.db = interface{}(tx).(sqlCommon) c.AddError(err) } else { - c.AddError(CantStartTransaction) + c.AddError(ErrCantStartTransaction) } return c } @@ -361,7 +381,7 @@ func (s *DB) Commit() *DB { if db, ok := s.db.(sqlTx); ok { s.AddError(db.Commit()) } else { - s.AddError(NoValidTransaction) + s.AddError(ErrInvalidTransaction) } return s } @@ -370,7 +390,7 @@ func (s *DB) Rollback() *DB { if db, ok := s.db.(sqlTx); ok { s.AddError(db.Rollback()) } else { - s.AddError(NoValidTransaction) + s.AddError(ErrInvalidTransaction) } return s } @@ -380,7 +400,7 @@ func (s *DB) NewRecord(value interface{}) bool { } func (s *DB) RecordNotFound() bool { - return s.Error == RecordNotFound + return s.Error == ErrRecordNotFound } // CreateTable create table for models @@ -541,7 +561,7 @@ func (s *DB) SetJoinTableHandler(source interface{}, column string, handler Join func (s *DB) AddError(err error) error { if err != nil { - if err != RecordNotFound { + if err != ErrRecordNotFound { if s.logMode == 0 { go s.print(fileWithLineNum(), err) } else { diff --git a/main_test.go b/main_test.go index c732e7fc..2da3de94 100644 --- a/main_test.go +++ b/main_test.go @@ -479,7 +479,7 @@ func TestRaw(t *testing.T) { } DB.Exec("update users set name=? where name in (?)", "jinzhu", []string{user1.Name, user2.Name, user3.Name}) - if DB.Where("name in (?)", []string{user1.Name, user2.Name, user3.Name}).First(&User{}).Error != gorm.RecordNotFound { + if DB.Where("name in (?)", []string{user1.Name, user2.Name, user3.Name}).First(&User{}).Error != gorm.ErrRecordNotFound { t.Error("Raw sql to update records") } } @@ -709,7 +709,7 @@ func TestOpenExistingDB(t *testing.T) { } var user User - if db.Where("name = ?", "jnfeinstein").First(&user).Error == gorm.RecordNotFound { + if db.Where("name = ?", "jnfeinstein").First(&user).Error == gorm.ErrRecordNotFound { t.Errorf("Should have found existing record") } } diff --git a/model.go b/model.go index ffa68b07..1ffdf2ef 100644 --- a/model.go +++ b/model.go @@ -2,6 +2,7 @@ package gorm import "time" +// Model base model definition, including `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt`, which could be embeded in your models type Model struct { ID uint `gorm:"primary_key"` CreatedAt time.Time diff --git a/model_struct.go b/model_struct.go index a17d2257..7773a1bf 100644 --- a/model_struct.go +++ b/model_struct.go @@ -12,6 +12,7 @@ import ( "github.com/jinzhu/inflection" ) +// DefaultTableNameHandler default table name handler var DefaultTableNameHandler = func(db *DB, defaultTableName string) string { return defaultTableName } @@ -39,6 +40,7 @@ func newModelStructsMap() *safeModelStructsMap { var modelStructsMap = newModelStructsMap() +// ModelStruct model definition type ModelStruct struct { PrimaryFields []*StructField StructFields []*StructField @@ -46,10 +48,12 @@ type ModelStruct struct { defaultTableName string } +// TableName get model's table name func (s *ModelStruct) TableName(db *DB) string { return DefaultTableNameHandler(db, s.defaultTableName) } +// StructField model field's struct definition type StructField struct { DBName string Name string @@ -506,6 +510,7 @@ func (scope *Scope) GetModelStruct() *ModelStruct { return &modelStruct } +// GetStructFields get model's field structs func (scope *Scope) GetStructFields() (fields []*StructField) { return scope.GetModelStruct().StructFields } diff --git a/preload_test.go b/preload_test.go index 8f21bc97..cde8e800 100644 --- a/preload_test.go +++ b/preload_test.go @@ -133,7 +133,7 @@ func TestNestedPreload1(t *testing.T) { t.Errorf("got %s; want %s", toJSONString(got), toJSONString(want)) } - if err := DB.Preload("Level2").Preload("Level2.Level1").Find(&got, "name = ?", "not_found").Error; err != gorm.RecordNotFound { + if err := DB.Preload("Level2").Preload("Level2.Level1").Find(&got, "name = ?", "not_found").Error; err != gorm.ErrRecordNotFound { t.Error(err) } } @@ -981,7 +981,7 @@ func TestNestedManyToManyPreload(t *testing.T) { t.Errorf("got %s; want %s", toJSONString(got), toJSONString(want)) } - if err := DB.Preload("Level2s.Level1s").Find(&got, "value = ?", "not_found").Error; err != gorm.RecordNotFound { + if err := DB.Preload("Level2s.Level1s").Find(&got, "value = ?", "not_found").Error; err != gorm.ErrRecordNotFound { t.Error(err) } } @@ -1038,7 +1038,7 @@ func TestNestedManyToManyPreload2(t *testing.T) { t.Errorf("got %s; want %s", toJSONString(got), toJSONString(want)) } - if err := DB.Preload("Level2.Level1s").Find(&got, "value = ?", "not_found").Error; err != gorm.RecordNotFound { + if err := DB.Preload("Level2.Level1s").Find(&got, "value = ?", "not_found").Error; err != gorm.ErrRecordNotFound { t.Error(err) } } diff --git a/scope.go b/scope.go index 9f4e821d..8d76489a 100644 --- a/scope.go +++ b/scope.go @@ -10,11 +10,12 @@ import ( "reflect" ) +// Scope contain any information of current operation when you perform any operation on the database type Scope struct { Search *search Value interface{} - Sql string - SqlVars []interface{} + SQL string + SQLVars []interface{} db *DB instanceID string primaryKeyField *Field @@ -23,6 +24,7 @@ type Scope struct { selectAttrs *[]string } +// IndirectValue return scope's reflect value's indirect value func (scope *Scope) IndirectValue() reflect.Value { return indirect(reflect.ValueOf(scope.Value)) } @@ -43,12 +45,13 @@ func (scope *Scope) NewDB() *DB { return nil } +// DB return scope's DB connection func (scope *Scope) DB() *DB { return scope.db } -// SqlDB return *sql.DB -func (scope *Scope) SqlDB() sqlCommon { +// SQLDB return *sql.DB +func (scope *Scope) SQLDB() sqlCommon { return scope.db.db } @@ -100,6 +103,7 @@ func (scope *Scope) HasError() bool { return scope.db.Error != nil } +// PrimaryFields return scope's primary fields func (scope *Scope) PrimaryFields() (fields []*Field) { for _, field := range scope.Fields() { if field.IsPrimaryKey { @@ -109,6 +113,7 @@ func (scope *Scope) PrimaryFields() (fields []*Field) { return fields } +// PrimaryField return scope's main primary field, if defined more that one primary fields, will return the one having column name `id` or the first one func (scope *Scope) PrimaryField() *Field { if primaryFields := scope.GetModelStruct().PrimaryFields; len(primaryFields) > 0 { if len(primaryFields) > 1 { @@ -241,8 +246,8 @@ func (scope *Scope) AddToVars(value interface{}) string { return exp } - scope.SqlVars = append(scope.SqlVars, value) - return scope.Dialect().BindVar(len(scope.SqlVars)) + scope.SQLVars = append(scope.SQLVars, value) + return scope.Dialect().BindVar(len(scope.SQLVars)) } type tabler interface { @@ -282,10 +287,10 @@ func (scope *Scope) QuotedTableName() (name string) { return scope.Quote(scope.TableName()) } -// CombinedConditionSql get combined condition sql +// CombinedConditionSql return combined condition sql func (scope *Scope) CombinedConditionSql() string { - return scope.joinsSql() + scope.whereSql() + scope.groupSql() + - scope.havingSql() + scope.orderSql() + scope.limitAndOffsetSql() + return scope.joinsSQL() + scope.whereSQL() + scope.groupSQL() + + scope.havingSQL() + scope.orderSQL() + scope.limitAndOffsetSQL() } // FieldByName find gorm.Field with name and db name @@ -308,7 +313,7 @@ func (scope *Scope) FieldByName(name string) (field *Field, ok bool) { // Raw set sql func (scope *Scope) Raw(sql string) *Scope { - scope.Sql = strings.Replace(sql, "$$", "?", -1) + scope.SQL = strings.Replace(sql, "$$", "?", -1) return scope } @@ -317,7 +322,7 @@ func (scope *Scope) Exec() *Scope { defer scope.trace(NowFunc()) if !scope.HasError() { - if result, err := scope.SqlDB().Exec(scope.Sql, scope.SqlVars...); scope.Err(err) == nil { + if result, err := scope.SQLDB().Exec(scope.SQL, scope.SQLVars...); scope.Err(err) == nil { if count, err := result.RowsAffected(); scope.Err(err) == nil { scope.db.RowsAffected = count } @@ -345,17 +350,19 @@ func (scope *Scope) InstanceID() string { return scope.instanceID } +// InstanceSet set value for current instance, but not for associations func (scope *Scope) InstanceSet(name string, value interface{}) *Scope { return scope.Set(name+scope.InstanceID(), value) } +// InstanceGet get setting from current instance func (scope *Scope) InstanceGet(name string) (interface{}, bool) { return scope.Get(name + scope.InstanceID()) } // Begin start a transaction func (scope *Scope) Begin() *Scope { - if db, ok := scope.SqlDB().(sqlDb); ok { + if db, ok := scope.SQLDB().(sqlDb); ok { if tx, err := db.Begin(); err == nil { scope.db.db = interface{}(tx).(sqlCommon) scope.InstanceSet("gorm:started_transaction", true) @@ -379,6 +386,7 @@ func (scope *Scope) CommitOrRollback() *Scope { return scope } +// SelectAttrs retur nselected attributes func (scope *Scope) SelectAttrs() []string { if scope.selectAttrs == nil { attrs := []string{} @@ -398,6 +406,7 @@ func (scope *Scope) SelectAttrs() []string { return *scope.selectAttrs } +// OmitAttrs return omited attributes func (scope *Scope) OmitAttrs() []string { return scope.Search.omits } diff --git a/scope_private.go b/scope_private.go index 31db4a0b..9309b6f4 100644 --- a/scope_private.go +++ b/scope_private.go @@ -76,7 +76,7 @@ func (scope *Scope) buildWhereCondition(clause map[string]interface{}) (str stri } func (scope *Scope) buildNotCondition(clause map[string]interface{}) (str string) { - var notEqualSql string + var notEqualSQL string var primaryKey = scope.PrimaryKey() switch value := clause["query"].(type) { @@ -87,10 +87,10 @@ func (scope *Scope) buildNotCondition(clause map[string]interface{}) (str string return fmt.Sprintf("(%v <> %v)", scope.Quote(primaryKey), id) } else if regexp.MustCompile("(?i) (=|<>|>|<|LIKE|IS|IN) ").MatchString(value) { str = fmt.Sprintf(" NOT (%v) ", value) - notEqualSql = fmt.Sprintf("NOT (%v)", value) + notEqualSQL = fmt.Sprintf("NOT (%v)", value) } else { str = fmt.Sprintf("(%v NOT IN (?))", scope.Quote(value)) - notEqualSql = fmt.Sprintf("(%v <> ?)", scope.Quote(value)) + notEqualSQL = fmt.Sprintf("(%v <> ?)", scope.Quote(value)) } case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, sql.NullInt64: return fmt.Sprintf("(%v <> %v)", scope.Quote(primaryKey), value) @@ -139,7 +139,7 @@ func (scope *Scope) buildNotCondition(clause map[string]interface{}) (str string if scanner, ok := interface{}(arg).(driver.Valuer); ok { arg, _ = scanner.Value() } - str = strings.Replace(notEqualSql, "?", scope.AddToVars(arg), 1) + str = strings.Replace(notEqualSQL, "?", scope.AddToVars(arg), 1) } } return @@ -173,7 +173,7 @@ func (scope *Scope) buildSelectQuery(clause map[string]interface{}) (str string) return } -func (scope *Scope) whereSql() (sql string) { +func (scope *Scope) whereSQL() (sql string) { var ( quotedTableName = scope.QuotedTableName() primaryConditions, andConditions, orConditions []string @@ -209,28 +209,28 @@ func (scope *Scope) whereSql() (sql string) { } } - orSql := strings.Join(orConditions, " OR ") - combinedSql := strings.Join(andConditions, " AND ") - if len(combinedSql) > 0 { - if len(orSql) > 0 { - combinedSql = combinedSql + " OR " + orSql + orSQL := strings.Join(orConditions, " OR ") + combinedSQL := strings.Join(andConditions, " AND ") + if len(combinedSQL) > 0 { + if len(orSQL) > 0 { + combinedSQL = combinedSQL + " OR " + orSQL } } else { - combinedSql = orSql + combinedSQL = orSQL } if len(primaryConditions) > 0 { sql = "WHERE " + strings.Join(primaryConditions, " AND ") - if len(combinedSql) > 0 { - sql = sql + " AND (" + combinedSql + ")" + if len(combinedSQL) > 0 { + sql = sql + " AND (" + combinedSQL + ")" } - } else if len(combinedSql) > 0 { - sql = "WHERE " + combinedSql + } else if len(combinedSQL) > 0 { + sql = "WHERE " + combinedSQL } return } -func (scope *Scope) selectSql() string { +func (scope *Scope) selectSQL() string { if len(scope.Search.selects) == 0 { if len(scope.Search.joinConditions) > 0 { return fmt.Sprintf("%v.*", scope.QuotedTableName()) @@ -240,25 +240,25 @@ func (scope *Scope) selectSql() string { return scope.buildSelectQuery(scope.Search.selects) } -func (scope *Scope) orderSql() string { +func (scope *Scope) orderSQL() string { if len(scope.Search.orders) == 0 || scope.Search.countingQuery { return "" } return " ORDER BY " + strings.Join(scope.Search.orders, ",") } -func (scope *Scope) limitAndOffsetSql() string { +func (scope *Scope) limitAndOffsetSQL() string { return scope.Dialect().LimitAndOffsetSQL(scope.Search.limit, scope.Search.offset) } -func (scope *Scope) groupSql() string { +func (scope *Scope) groupSQL() string { if len(scope.Search.group) == 0 { return "" } return " GROUP BY " + scope.Search.group } -func (scope *Scope) havingSql() string { +func (scope *Scope) havingSQL() string { if len(scope.Search.havingConditions) == 0 { return "" } @@ -270,15 +270,15 @@ func (scope *Scope) havingSql() string { } } - combinedSql := strings.Join(andConditions, " AND ") - if len(combinedSql) == 0 { + combinedSQL := strings.Join(andConditions, " AND ") + if len(combinedSQL) == 0 { return "" } - return " HAVING " + combinedSql + return " HAVING " + combinedSQL } -func (scope *Scope) joinsSql() string { +func (scope *Scope) joinsSQL() string { var joinConditions []string for _, clause := range scope.Search.joinConditions { if sql := scope.buildWhereCondition(clause); sql != "" { @@ -289,11 +289,11 @@ func (scope *Scope) joinsSql() string { return strings.Join(joinConditions, " ") + " " } -func (scope *Scope) prepareQuerySql() { +func (scope *Scope) prepareQuerySQL() { if scope.Search.raw { scope.Raw(strings.TrimSuffix(strings.TrimPrefix(scope.CombinedConditionSql(), " WHERE ("), ")")) } else { - scope.Raw(fmt.Sprintf("SELECT %v FROM %v %v", scope.selectSql(), scope.QuotedTableName(), scope.CombinedConditionSql())) + scope.Raw(fmt.Sprintf("SELECT %v FROM %v %v", scope.selectSQL(), scope.QuotedTableName(), scope.CombinedConditionSql())) } return } @@ -345,15 +345,15 @@ func (scope *Scope) updatedAttrsWithValues(values map[string]interface{}) (resul func (scope *Scope) row() *sql.Row { defer scope.trace(NowFunc()) scope.callCallbacks(scope.db.parent.callbacks.rowQueries) - scope.prepareQuerySql() - return scope.SqlDB().QueryRow(scope.Sql, scope.SqlVars...) + scope.prepareQuerySQL() + return scope.SQLDB().QueryRow(scope.SQL, scope.SQLVars...) } func (scope *Scope) rows() (*sql.Rows, error) { defer scope.trace(NowFunc()) scope.callCallbacks(scope.db.parent.callbacks.rowQueries) - scope.prepareQuerySql() - return scope.SqlDB().Query(scope.Sql, scope.SqlVars...) + scope.prepareQuerySQL() + return scope.SQLDB().Query(scope.SQL, scope.SQLVars...) } func (scope *Scope) initialize() *Scope { @@ -404,8 +404,8 @@ func (scope *Scope) typeName() string { // trace print sql log func (scope *Scope) trace(t time.Time) { - if len(scope.Sql) > 0 { - scope.db.slog(scope.Sql, t, scope.SqlVars...) + if len(scope.SQL) > 0 { + scope.db.slog(scope.SQL, t, scope.SQLVars...) } } @@ -599,7 +599,7 @@ func (scope *Scope) addIndex(unique bool, indexName string, column ...string) { sqlCreate = "CREATE UNIQUE INDEX" } - scope.Raw(fmt.Sprintf("%s %v ON %v(%v) %v", sqlCreate, indexName, scope.QuotedTableName(), strings.Join(columns, ", "), scope.whereSql())).Exec() + scope.Raw(fmt.Sprintf("%s %v ON %v(%v) %v", sqlCreate, indexName, scope.QuotedTableName(), strings.Join(columns, ", "), scope.whereSQL())).Exec() } func (scope *Scope) addForeignKey(field string, dest string, onDelete string, onUpdate string) { diff --git a/search.go b/search.go index 4e31ae03..078bd429 100644 --- a/search.go +++ b/search.go @@ -93,7 +93,7 @@ func (s *search) Offset(offset int) *search { } func (s *search) Group(query string) *search { - s.group = s.getInterfaceAsSql(query) + s.group = s.getInterfaceAsSQL(query) return s } @@ -134,12 +134,12 @@ func (s *search) Table(name string) *search { return s } -func (s *search) getInterfaceAsSql(value interface{}) (str string) { +func (s *search) getInterfaceAsSQL(value interface{}) (str string) { switch value.(type) { case string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: str = fmt.Sprintf("%v", value) default: - s.db.AddError(InvalidSql) + s.db.AddError(ErrInvalidSQL) } if str == "-1" { diff --git a/utils.go b/utils.go index 7fc53fa5..4ac2ab10 100644 --- a/utils.go +++ b/utils.go @@ -9,8 +9,19 @@ import ( "runtime" "strings" "sync" + "time" ) +// NowFunc returns current time, this function is exported in order to be able +// to give the flexibility to the developer to customize it according to their +// needs +// +// e.g: return time.Now().UTC() +// +var NowFunc = func() time.Time { + return time.Now() +} + // Copied from golint var commonInitialisms = []string{"API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "LHS", "QPS", "RAM", "RHS", "RPC", "SLA", "SMTP", "SSH", "TLS", "TTL", "UI", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XSRF", "XSS"} var commonInitialismsReplacer *strings.Replacer From 779c4d436e888c233c1aca92cd7b03548d2d86bf Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 7 Mar 2016 18:43:15 +0800 Subject: [PATCH 77/83] Refactor based on golint --- main.go | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/main.go b/main.go index b4b8a169..2dbfb411 100644 --- a/main.go +++ b/main.go @@ -195,6 +195,8 @@ func (s *DB) Joins(query string, args ...interface{}) *DB { return s.clone().search.Joins(query, args...).db } +// Scopes pass current database connection to arguments `func(*DB) *DB`, which could be used to add conditions dynamically +// Refer https://jinzhu.github.io/gorm/curd.html#scopes for more func (s *DB) Scopes(funcs ...func(*DB) *DB) *DB { for _, f := range funcs { s = f(s) @@ -202,18 +204,22 @@ func (s *DB) Scopes(funcs ...func(*DB) *DB) *DB { return s } +// Unscoped return all record including deleted record, refer Soft Delete https://jinzhu.github.io/gorm/curd.html#scopes func (s *DB) Unscoped() *DB { return s.clone().search.unscoped().db } +// Attrs initalize struct with argument if record not found with `FirstOrInit` https://jinzhu.github.io/gorm/curd.html#firstorinit or `FirstOrCreate` https://jinzhu.github.io/gorm/curd.html#firstorcreate func (s *DB) Attrs(attrs ...interface{}) *DB { return s.clone().search.Attrs(attrs...).db } +// Assign assign result with argument regardless it is found or not with `FirstOrInit` https://jinzhu.github.io/gorm/curd.html#firstorinit or `FirstOrCreate` https://jinzhu.github.io/gorm/curd.html#firstorcreate func (s *DB) Assign(attrs ...interface{}) *DB { return s.clone().search.Assign(attrs...).db } +// First find first record that match given conditions, order by primary key func (s *DB) First(out interface{}, where ...interface{}) *DB { newScope := s.clone().NewScope(out) newScope.Search.Limit(1) @@ -221,6 +227,7 @@ func (s *DB) First(out interface{}, where ...interface{}) *DB { inlineCondition(where...).callCallbacks(s.parent.callbacks.queries).db } +// Last find last record that match given conditions, order by primary key func (s *DB) Last(out interface{}, where ...interface{}) *DB { newScope := s.clone().NewScope(out) newScope.Search.Limit(1) @@ -228,26 +235,31 @@ func (s *DB) Last(out interface{}, where ...interface{}) *DB { inlineCondition(where...).callCallbacks(s.parent.callbacks.queries).db } +// Find find records that match given conditions func (s *DB) Find(out interface{}, where ...interface{}) *DB { return s.clone().NewScope(out).inlineCondition(where...).callCallbacks(s.parent.callbacks.queries).db } +// Scan scan value to a struct func (s *DB) Scan(dest interface{}) *DB { return s.clone().NewScope(s.Value).Set("gorm:query_destination", dest).callCallbacks(s.parent.callbacks.queries).db } +// Row return `*sql.Row` with given condtions func (s *DB) Row() *sql.Row { return s.NewScope(s.Value).row() } +// Rows return `*sql.Rows` with given condtions func (s *DB) Rows() (*sql.Rows, error) { return s.NewScope(s.Value).rows() } -func (s *DB) ScanRows(rows *sql.Rows, value interface{}) error { +// ScanRows scan `*sql.Rows` to give struct +func (s *DB) ScanRows(rows *sql.Rows, result interface{}) error { var ( clone = s.clone() - scope = clone.NewScope(value) + scope = clone.NewScope(result) columns, err = rows.Columns() ) @@ -258,18 +270,22 @@ func (s *DB) ScanRows(rows *sql.Rows, value interface{}) error { return clone.Error } +// Pluck used to query single column from a model as a map func (s *DB) Pluck(column string, value interface{}) *DB { return s.NewScope(s.Value).pluck(column, value).db } +// Count get how many records for a model func (s *DB) Count(value interface{}) *DB { return s.NewScope(s.Value).count(value).db } +// Related get related associations func (s *DB) Related(value interface{}, foreignKeys ...string) *DB { return s.clone().NewScope(s.Value).related(value, foreignKeys...).db } +// FirstOrInit find first matched record or initalize a new one with given conditions (only works with struct, map conditions) func (s *DB) FirstOrInit(out interface{}, where ...interface{}) *DB { c := s.clone() if result := c.First(out, where...); result.Error != nil { @@ -283,6 +299,7 @@ func (s *DB) FirstOrInit(out interface{}, where ...interface{}) *DB { return c } +// FirstOrCreate find first matched record or create a new one with given conditions (only works with struct, map conditions) func (s *DB) FirstOrCreate(out interface{}, where ...interface{}) *DB { c := s.clone() if result := c.First(out, where...); result.Error != nil { @@ -296,10 +313,12 @@ func (s *DB) FirstOrCreate(out interface{}, where ...interface{}) *DB { return c } +// Update update attributes with callbacks, refer: https://jinzhu.github.io/gorm/curd.html#update func (s *DB) Update(attrs ...interface{}) *DB { return s.Updates(toSearchableMap(attrs...), true) } +// Updates update attributes with callbacks, refer: https://jinzhu.github.io/gorm/curd.html#update func (s *DB) Updates(values interface{}, ignoreProtectedAttrs ...bool) *DB { return s.clone().NewScope(s.Value). Set("gorm:ignore_protected_attrs", len(ignoreProtectedAttrs) > 0). @@ -307,10 +326,12 @@ func (s *DB) Updates(values interface{}, ignoreProtectedAttrs ...bool) *DB { callCallbacks(s.parent.callbacks.updates).db } +// UpdateColumn update attributes without callbacks, refer: https://jinzhu.github.io/gorm/curd.html#update func (s *DB) UpdateColumn(attrs ...interface{}) *DB { return s.UpdateColumns(toSearchableMap(attrs...)) } +// UpdateColumns update attributes without callbacks, refer: https://jinzhu.github.io/gorm/curd.html#update func (s *DB) UpdateColumns(values interface{}) *DB { return s.clone().NewScope(s.Value). Set("gorm:update_column", true). @@ -319,6 +340,7 @@ func (s *DB) UpdateColumns(values interface{}) *DB { callCallbacks(s.parent.callbacks.updates).db } +// Save update the value in database, if the value doesn't have primary key, will insert it func (s *DB) Save(value interface{}) *DB { scope := s.clone().NewScope(value) if scope.PrimaryKeyZero() { @@ -327,19 +349,24 @@ func (s *DB) Save(value interface{}) *DB { return scope.callCallbacks(s.parent.callbacks.updates).db } +// Create insert the value into database func (s *DB) Create(value interface{}) *DB { scope := s.clone().NewScope(value) return scope.callCallbacks(s.parent.callbacks.creates).db } +// Delete delete value that match given conditions, if the value has primary key, then will including the primary key as condition func (s *DB) Delete(value interface{}, where ...interface{}) *DB { return s.clone().NewScope(value).inlineCondition(where...).callCallbacks(s.parent.callbacks.deletes).db } +// Raw use raw sql as conditions, won't run it unless invoked by other methods +// db.Raw("SELECT name, age FROM users WHERE name = ?", 3).Scan(&result) func (s *DB) Raw(sql string, values ...interface{}) *DB { return s.clone().search.Raw(true).Where(sql, values...).db } +// Exec execute raw sql func (s *DB) Exec(sql string, values ...interface{}) *DB { scope := s.clone().NewScope(nil) generatedSQL := scope.buildWhereCondition(map[string]interface{}{"query": sql, "args": values}) @@ -348,12 +375,18 @@ func (s *DB) Exec(sql string, values ...interface{}) *DB { return scope.Exec().db } +// Model specify the model you would like to run db operations +// // update all users's name to `hello` +// db.Model(&User{}).Update("name", "hello") +// // if user's primary key is non-blank, will use it as condition, then will only update the user's name to `hello` +// db.Model(&user).Update("name", "hello") func (s *DB) Model(value interface{}) *DB { c := s.clone() c.Value = value return c } +// Table specify the table you would like to run db operations func (s *DB) Table(name string) *DB { clone := s.clone() clone.search.Table(name) @@ -361,10 +394,12 @@ func (s *DB) Table(name string) *DB { return clone } +// Debug start debug mode func (s *DB) Debug() *DB { return s.clone().LogMode(true) } +// Begin begin a transaction func (s *DB) Begin() *DB { c := s.clone() if db, ok := c.db.(sqlDb); ok { @@ -377,6 +412,7 @@ func (s *DB) Begin() *DB { return c } +// Commit commit a transaction func (s *DB) Commit() *DB { if db, ok := s.db.(sqlTx); ok { s.AddError(db.Commit()) @@ -386,6 +422,7 @@ func (s *DB) Commit() *DB { return s } +// Rollback rollback a transaction func (s *DB) Rollback() *DB { if db, ok := s.db.(sqlTx); ok { s.AddError(db.Rollback()) @@ -395,10 +432,12 @@ func (s *DB) Rollback() *DB { return s } +// NewRecord check if value's primary key is blank or not func (s *DB) NewRecord(value interface{}) bool { return s.clone().NewScope(value).PrimaryKeyZero() } +// RecordNotFound check if returning record not found error func (s *DB) RecordNotFound() bool { return s.Error == ErrRecordNotFound } @@ -438,6 +477,7 @@ func (s *DB) DropTableIfExists(values ...interface{}) *DB { return db } +// HasTable check has table or not func (s *DB) HasTable(value interface{}) bool { var ( scope = s.clone().NewScope(value) @@ -455,6 +495,7 @@ func (s *DB) HasTable(value interface{}) bool { return has } +// AutoMigrate run auto migration for given models, will only add missing fields, won't delete/change current data func (s *DB) AutoMigrate(values ...interface{}) *DB { db := s.clone() for _, value := range values { @@ -463,30 +504,35 @@ func (s *DB) AutoMigrate(values ...interface{}) *DB { return db } +// ModifyColumn modify column to type func (s *DB) ModifyColumn(column string, typ string) *DB { scope := s.clone().NewScope(s.Value) scope.modifyColumn(column, typ) return scope.db } +// DropColumn drop a column func (s *DB) DropColumn(column string) *DB { scope := s.clone().NewScope(s.Value) scope.dropColumn(column) return scope.db } -func (s *DB) AddIndex(indexName string, column ...string) *DB { +// AddIndex add index for columns with given name +func (s *DB) AddIndex(indexName string, columns ...string) *DB { scope := s.Unscoped().NewScope(s.Value) - scope.addIndex(false, indexName, column...) + scope.addIndex(false, indexName, columns...) return scope.db } -func (s *DB) AddUniqueIndex(indexName string, column ...string) *DB { +// AddUniqueIndex add unique index for columns with given name +func (s *DB) AddUniqueIndex(indexName string, columns ...string) *DB { scope := s.clone().NewScope(s.Value) - scope.addIndex(true, indexName, column...) + scope.addIndex(true, indexName, columns...) return scope.db } +// RemoveIndex remove index with name func (s *DB) RemoveIndex(indexName string) *DB { scope := s.clone().NewScope(s.Value) scope.removeIndex(indexName) @@ -501,6 +547,7 @@ func (s *DB) AddForeignKey(field string, dest string, onDelete string, onUpdate return scope.db } +// Association start `Association Mode` to handler relations things easir in that mode, refer: https://jinzhu.github.io/gorm/associations.html#association-mode func (s *DB) Association(column string) *Association { var err error scope := s.clone().NewScope(s.Value) @@ -522,26 +569,29 @@ func (s *DB) Association(column string) *Association { return &Association{Error: err} } +// Preload preload column with given conditions func (s *DB) Preload(column string, conditions ...interface{}) *DB { return s.clone().search.Preload(column, conditions...).db } -// Set set value by name +// Set set setting by name, which could be used in callbacks, will clone a new db, and update its setting func (s *DB) Set(name string, value interface{}) *DB { return s.clone().InstantSet(name, value) } +// InstantSet instant set setting, will affect current db func (s *DB) InstantSet(name string, value interface{}) *DB { s.values[name] = value return s } -// Get get value by name +// Get get setting by name func (s *DB) Get(name string) (value interface{}, ok bool) { value, ok = s.values[name] return } +// SetJoinTableHandler set a model's join table handler for a relation func (s *DB) SetJoinTableHandler(source interface{}, column string, handler JoinTableHandlerInterface) { scope := s.NewScope(source) for _, field := range scope.GetModelStruct().StructFields { @@ -559,6 +609,7 @@ func (s *DB) SetJoinTableHandler(source interface{}, column string, handler Join } } +// AddError add error to the db func (s *DB) AddError(err error) error { if err != nil { if err != ErrRecordNotFound { @@ -580,6 +631,7 @@ func (s *DB) AddError(err error) error { return err } +// GetErrors get happened errors for the db func (s *DB) GetErrors() (errors []error) { if errs, ok := s.Error.(errorsInterface); ok { return errs.GetErrors() From 88184a989edcd70d81f579b83cd7f0148cf8aeae Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 7 Mar 2016 21:09:05 +0800 Subject: [PATCH 78/83] Update godoc --- callback.go | 18 +++++----- dialect.go | 2 +- errors.go | 4 +-- main.go | 89 ++++++++++++++++++++++++++---------------------- model.go | 5 ++- model_struct.go | 2 +- scope.go | 32 ++++++++--------- scope_private.go | 7 ---- utils.go | 10 +++--- 9 files changed, 87 insertions(+), 82 deletions(-) diff --git a/callback.go b/callback.go index 9704e584..84526236 100644 --- a/callback.go +++ b/callback.go @@ -7,13 +7,13 @@ import ( // DefaultCallback default callbacks defined by gorm var DefaultCallback = &Callback{} -// Callback contains callbacks that used when CURD objects -// Field `creates` hold callbacks will be call when creating object -// Field `updates` hold callbacks will be call when updating object -// Field `deletes` hold callbacks will be call when deleting object -// Field `queries` hold callbacks will be call when querying object with query methods like Find, First, Related, Association... -// Field `rowQueries` hold callbacks will be call when querying object with Row, Rows... -// Field `processors` hold all callback processors, will be used to generate above callbacks in order +// Callback is a struct that contains all CURD callbacks +// Field `creates` contains callbacks will be call when creating object +// Field `updates` contains callbacks will be call when updating object +// Field `deletes` contains callbacks will be call when deleting object +// Field `queries` contains callbacks will be call when querying object with query methods like Find, First, Related, Association... +// Field `rowQueries` contains callbacks will be call when querying object with Row, Rows... +// Field `processors` contains all callback processors, will be used to generate above callbacks in order type Callback struct { creates []*func(scope *Scope) updates []*func(scope *Scope) @@ -23,7 +23,7 @@ type Callback struct { processors []*CallbackProcessor } -// CallbackProcessor contains all informations for a callback +// CallbackProcessor contains callback informations type CallbackProcessor struct { name string // current callback's name before string // register current callback before a callback @@ -68,7 +68,7 @@ func (c *Callback) Delete() *CallbackProcessor { } // Query could be used to register callbacks for querying objects with query methods like `Find`, `First`, `Related`, `Association`... -// refer `Create` for usage +// Refer `Create` for usage func (c *Callback) Query() *CallbackProcessor { return &CallbackProcessor{kind: "query", parent: c} } diff --git a/dialect.go b/dialect.go index 1d757078..6c9405da 100644 --- a/dialect.go +++ b/dialect.go @@ -34,7 +34,7 @@ type Dialect interface { // HasColumn check has column or not HasColumn(tableName string, columnName string) bool - // LimitAndOffsetSQL return generate SQL with limit and offset, as mssql has special case + // LimitAndOffsetSQL return generated SQL with Limit and Offset, as mssql has special case LimitAndOffsetSQL(limit, offset int) string // SelectFromDummyTable return select values, for most dbs, `SELECT values` just works, mysql needs `SELECT value FROM DUAL` SelectFromDummyTable() string diff --git a/errors.go b/errors.go index 32b7f220..cc66567d 100644 --- a/errors.go +++ b/errors.go @@ -6,9 +6,9 @@ import ( ) var ( - // ErrRecordNotFound record not found, happens when you are looking up with a struct, and haven't find any matched data + // ErrRecordNotFound record not found error, happens when haven't find any matched data when looking up with a struct ErrRecordNotFound = errors.New("record not found") - // ErrInvalidSQL invalid SQL, happens when you passed invalid SQL + // ErrInvalidSQL invalid SQL error, happens when you passed invalid SQL ErrInvalidSQL = errors.New("invalid SQL") // ErrInvalidTransaction invalid transaction when you are trying to `Commit` or `Rollback` ErrInvalidTransaction = errors.New("no valid transaction") diff --git a/main.go b/main.go index 2dbfb411..8baf1deb 100644 --- a/main.go +++ b/main.go @@ -26,14 +26,17 @@ type DB struct { joinTableHandlers map[string]JoinTableHandler } -// Open open a new db connection, need to import driver first, for example: +// Open initialize a new db connection, need to import driver first, e.g: // // import _ "github.com/go-sql-driver/mysql" // func main() { // db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local") // } -// GORM has wrapped some drivers, for easier to remember its name, so you could import the mysql driver with +// GORM has wrapped some drivers, for easier to remember driver's import path, so you could import the mysql driver with // import _ "github.com/jinzhu/gorm/dialects/mysql" +// // import _ "github.com/jinzhu/gorm/dialects/postgres" +// // import _ "github.com/jinzhu/gorm/dialects/sqlite" +// // import _ "github.com/jinzhu/gorm/dialects/mssql" func Open(dialect string, args ...interface{}) (*DB, error) { var db DB var err error @@ -87,7 +90,7 @@ func (s *DB) DB() *sql.DB { return s.db.(*sql.DB) } -// New initialize a new db connection without any search conditions +// New clone a new db connection without search conditions func (s *DB) New() *DB { clone := s.clone() clone.search = nil @@ -102,16 +105,14 @@ func (s *DB) NewScope(value interface{}) *Scope { return &Scope{db: dbClone, Search: dbClone.search.clone(), Value: value} } -// CommonDB return the underlying sql.DB or sql.Tx instance. -// Use of this method is discouraged. It's mainly intended to allow -// coexistence with legacy non-GORM code. +// CommonDB return the underlying `*sql.DB` or `*sql.Tx` instance, mainly intended to allow coexistence with legacy non-GORM code. func (s *DB) CommonDB() sqlCommon { return s.db } -// Callback return Callbacks container, you could add/remove/change callbacks with it +// Callback return `Callbacks` container, you could add/change/delete callbacks with it // db.Callback().Create().Register("update_created_at", updateCreated) -// Refer: https://jinzhu.github.io/gorm/development.html#callbacks for more +// Refer https://jinzhu.github.io/gorm/development.html#callbacks func (s *DB) Callback() *Callback { s.parent.callbacks = s.parent.callbacks.clone() return s.parent.callbacks @@ -138,17 +139,17 @@ func (s *DB) SingularTable(enable bool) { s.parent.singularTable = enable } -// Where return a new relation, accepts use `map`, `struct` or `string` as conditions, refer http://jinzhu.github.io/gorm/curd.html#query +// Where return a new relation, filter records with given conditions, accepts `map`, `struct` or `string` as conditions, refer http://jinzhu.github.io/gorm/curd.html#query func (s *DB) Where(query interface{}, args ...interface{}) *DB { return s.clone().search.Where(query, args...).db } -// Or match before conditions or this one, similar to `Where` +// Or filter records that match before conditions or this one, similar to `Where` func (s *DB) Or(query interface{}, args ...interface{}) *DB { return s.clone().search.Or(query, args...).db } -// Not don't match current conditions, similar to `Where` +// Not filter records that don't match current conditions, similar to `Where` func (s *DB) Not(query interface{}, args ...interface{}) *DB { return s.clone().search.Not(query, args...).db } @@ -163,18 +164,18 @@ func (s *DB) Offset(offset int) *DB { return s.clone().search.Offset(offset).db } -// Order specify order when retrieve records from database, pass `true` as the second argument to overwrite `Order` conditions +// Order specify order when retrieve records from database, set reorder to `true` to overwrite defined conditions func (s *DB) Order(value string, reorder ...bool) *DB { return s.clone().search.Order(value, reorder...).db } -// Select When querying, specify fields that you want to retrieve from database, by default, will select all fields; +// Select specify fields that you want to retrieve from database when querying, by default, will select all fields; // When creating/updating, specify fields that you want to save to database func (s *DB) Select(query interface{}, args ...interface{}) *DB { return s.clone().search.Select(query, args...).db } -// Omit specify fields that you want to ignore when save to database when creating/updating +// Omit specify fields that you want to ignore when saving to database for creating, updating func (s *DB) Omit(columns ...string) *DB { return s.clone().search.Omit(columns...).db } @@ -196,7 +197,18 @@ func (s *DB) Joins(query string, args ...interface{}) *DB { } // Scopes pass current database connection to arguments `func(*DB) *DB`, which could be used to add conditions dynamically -// Refer https://jinzhu.github.io/gorm/curd.html#scopes for more +// func AmountGreaterThan1000(db *gorm.DB) *gorm.DB { +// return db.Where("amount > ?", 1000) +// } +// +// func OrderStatus(status []string) func (db *gorm.DB) *gorm.DB { +// return func (db *gorm.DB) *gorm.DB { +// return db.Scopes(AmountGreaterThan1000).Where("status in (?)", status) +// } +// } +// +// db.Scopes(AmountGreaterThan1000, OrderStatus([]string{"paid", "shipped"})).Find(&orders) +// Refer https://jinzhu.github.io/gorm/curd.html#scopes func (s *DB) Scopes(funcs ...func(*DB) *DB) *DB { for _, f := range funcs { s = f(s) @@ -204,7 +216,7 @@ func (s *DB) Scopes(funcs ...func(*DB) *DB) *DB { return s } -// Unscoped return all record including deleted record, refer Soft Delete https://jinzhu.github.io/gorm/curd.html#scopes +// Unscoped return all record including deleted record, refer Soft Delete https://jinzhu.github.io/gorm/curd.html#soft-delete func (s *DB) Unscoped() *DB { return s.clone().search.unscoped().db } @@ -245,12 +257,12 @@ func (s *DB) Scan(dest interface{}) *DB { return s.clone().NewScope(s.Value).Set("gorm:query_destination", dest).callCallbacks(s.parent.callbacks.queries).db } -// Row return `*sql.Row` with given condtions +// Row return `*sql.Row` with given conditions func (s *DB) Row() *sql.Row { return s.NewScope(s.Value).row() } -// Rows return `*sql.Rows` with given condtions +// Rows return `*sql.Rows` with given conditions func (s *DB) Rows() (*sql.Rows, error) { return s.NewScope(s.Value).rows() } @@ -271,6 +283,8 @@ func (s *DB) ScanRows(rows *sql.Rows, result interface{}) error { } // Pluck used to query single column from a model as a map +// var ages []int64 +// db.Find(&users).Pluck("age", &ages) func (s *DB) Pluck(column string, value interface{}) *DB { return s.NewScope(s.Value).pluck(column, value).db } @@ -286,6 +300,7 @@ func (s *DB) Related(value interface{}, foreignKeys ...string) *DB { } // FirstOrInit find first matched record or initalize a new one with given conditions (only works with struct, map conditions) +// https://jinzhu.github.io/gorm/curd.html#firstorinit func (s *DB) FirstOrInit(out interface{}, where ...interface{}) *DB { c := s.clone() if result := c.First(out, where...); result.Error != nil { @@ -300,6 +315,7 @@ func (s *DB) FirstOrInit(out interface{}, where ...interface{}) *DB { } // FirstOrCreate find first matched record or create a new one with given conditions (only works with struct, map conditions) +// https://jinzhu.github.io/gorm/curd.html#firstorcreate func (s *DB) FirstOrCreate(out interface{}, where ...interface{}) *DB { c := s.clone() if result := c.First(out, where...); result.Error != nil { @@ -340,7 +356,7 @@ func (s *DB) UpdateColumns(values interface{}) *DB { callCallbacks(s.parent.callbacks.updates).db } -// Save update the value in database, if the value doesn't have primary key, will insert it +// Save update value in database, if the value doesn't have primary key, will insert it func (s *DB) Save(value interface{}) *DB { scope := s.clone().NewScope(value) if scope.PrimaryKeyZero() { @@ -355,7 +371,7 @@ func (s *DB) Create(value interface{}) *DB { return scope.callCallbacks(s.parent.callbacks.creates).db } -// Delete delete value that match given conditions, if the value has primary key, then will including the primary key as condition +// Delete delete value match given conditions, if the value has primary key, then will including the primary key as condition func (s *DB) Delete(value interface{}, where ...interface{}) *DB { return s.clone().NewScope(value).inlineCondition(where...).callCallbacks(s.parent.callbacks.deletes).db } @@ -432,14 +448,19 @@ func (s *DB) Rollback() *DB { return s } -// NewRecord check if value's primary key is blank or not +// NewRecord check if value's primary key is blank func (s *DB) NewRecord(value interface{}) bool { return s.clone().NewScope(value).PrimaryKeyZero() } -// RecordNotFound check if returning record not found error +// RecordNotFound check if returning ErrRecordNotFound error func (s *DB) RecordNotFound() bool { - return s.Error == ErrRecordNotFound + for _, err := range s.GetErrors() { + if err == ErrRecordNotFound { + return true + } + } + return false } // CreateTable create table for models @@ -464,19 +485,6 @@ func (s *DB) DropTable(values ...interface{}) *DB { return db } -// DropTableIfExists drop table for models only when it exists -func (s *DB) DropTableIfExists(values ...interface{}) *DB { - db := s.clone() - for _, value := range values { - if tableName, ok := value.(string); ok { - db = db.Table(tableName) - } - - db = db.NewScope(value).dropTableIfExists().db - } - return db -} - // HasTable check has table or not func (s *DB) HasTable(value interface{}) bool { var ( @@ -539,8 +547,8 @@ func (s *DB) RemoveIndex(indexName string) *DB { return scope.db } -// AddForeignKey Add foreign key to the given scope -// Example: db.Model(&User{}).AddForeignKey("city_id", "cities(id)", "RESTRICT", "RESTRICT") +// AddForeignKey Add foreign key to the given scope, e.g: +// db.Model(&User{}).AddForeignKey("city_id", "cities(id)", "RESTRICT", "RESTRICT") func (s *DB) AddForeignKey(field string, dest string, onDelete string, onUpdate string) *DB { scope := s.clone().NewScope(s.Value) scope.addForeignKey(field, dest, onDelete, onUpdate) @@ -569,7 +577,8 @@ func (s *DB) Association(column string) *Association { return &Association{Error: err} } -// Preload preload column with given conditions +// Preload preload associations with given conditions +// db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users) func (s *DB) Preload(column string, conditions ...interface{}) *DB { return s.clone().search.Preload(column, conditions...).db } @@ -631,7 +640,7 @@ func (s *DB) AddError(err error) error { return err } -// GetErrors get happened errors for the db +// GetErrors get happened errors from the db func (s *DB) GetErrors() (errors []error) { if errs, ok := s.Error.(errorsInterface); ok { return errs.GetErrors() diff --git a/model.go b/model.go index 1ffdf2ef..a6650877 100644 --- a/model.go +++ b/model.go @@ -2,7 +2,10 @@ package gorm import "time" -// Model base model definition, including `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt`, which could be embeded in your models +// Model base model definition, including fields `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt`, which could be embeded in your models +// type User struct { +// gorm.Model +// } type Model struct { ID uint `gorm:"primary_key"` CreatedAt time.Time diff --git a/model_struct.go b/model_struct.go index 7773a1bf..b0dccf3b 100644 --- a/model_struct.go +++ b/model_struct.go @@ -109,7 +109,7 @@ func getForeignField(column string, fields []*StructField) *StructField { return nil } -// GetModelStruct generate model struct & relationships based on struct and tag definition +// GetModelStruct get value's model struct, relationships based on struct and tag definition func (scope *Scope) GetModelStruct() *ModelStruct { var modelStruct ModelStruct // Scope value can't be nil diff --git a/scope.go b/scope.go index 8d76489a..6239db7a 100644 --- a/scope.go +++ b/scope.go @@ -10,7 +10,7 @@ import ( "reflect" ) -// Scope contain any information of current operation when you perform any operation on the database +// Scope contain current operation's information when you perform any operation on the database type Scope struct { Search *search Value interface{} @@ -60,7 +60,7 @@ func (scope *Scope) SkipLeft() { scope.skipLeft = true } -// Quote used to quote database column name according to database dialect +// Quote used to quote string to escape them for database func (scope *Scope) Quote(str string) string { if strings.Index(str, ".") != -1 { newStrs := []string{} @@ -85,7 +85,7 @@ func (scope *Scope) Dialect() Dialect { return scope.db.parent.dialect } -// Err write error +// Err add error to Scope func (scope *Scope) Err(err error) error { if err != nil { scope.db.AddError(err) @@ -126,7 +126,7 @@ func (scope *Scope) PrimaryField() *Field { return nil } -// PrimaryKey get the primary key's column name +// PrimaryKey get main primary field's db name func (scope *Scope) PrimaryKey() string { if field := scope.PrimaryField(); field != nil { return field.DBName @@ -134,7 +134,7 @@ func (scope *Scope) PrimaryKey() string { return "" } -// PrimaryKeyZero check the primary key is blank or not +// PrimaryKeyZero check main primary field's value is blank or not func (scope *Scope) PrimaryKeyZero() bool { field := scope.PrimaryField() return field == nil || field.IsBlank @@ -158,7 +158,7 @@ func (scope *Scope) HasColumn(column string) bool { return false } -// SetColumn to set the column's value +// SetColumn to set the column's value, column could be field or field's name/dbname func (scope *Scope) SetColumn(column interface{}, value interface{}) error { var updateAttrs = map[string]interface{}{} if attrs, ok := scope.InstanceGet("gorm:update_attrs"); ok { @@ -221,7 +221,7 @@ func (scope *Scope) callMethod(methodName string, reflectValue reflect.Value) { } } -// CallMethod call scope value's method, if it is a slice, will call value's method one by one +// CallMethod call scope value's method, if it is a slice, will call its element's method one by one func (scope *Scope) CallMethod(methodName string) { if scope.Value == nil { return @@ -236,7 +236,7 @@ func (scope *Scope) CallMethod(methodName string) { } } -// AddToVars add value as sql's vars, gorm will escape them +// AddToVars add value as sql's vars, used to prevent SQL injection func (scope *Scope) AddToVars(value interface{}) string { if expr, ok := value.(*expr); ok { exp := expr.expr @@ -293,7 +293,7 @@ func (scope *Scope) CombinedConditionSql() string { scope.havingSQL() + scope.orderSQL() + scope.limitAndOffsetSQL() } -// FieldByName find gorm.Field with name and db name +// FieldByName find `gorm.Field` with field name or db name func (scope *Scope) FieldByName(name string) (field *Field, ok bool) { var ( dbName = ToDBName(name) @@ -311,13 +311,13 @@ func (scope *Scope) FieldByName(name string) (field *Field, ok bool) { return mostMatchedField, mostMatchedField != nil } -// Raw set sql +// Raw set raw sql func (scope *Scope) Raw(sql string) *Scope { scope.SQL = strings.Replace(sql, "$$", "?", -1) return scope } -// Exec invoke sql +// Exec perform generated SQL func (scope *Scope) Exec() *Scope { defer scope.trace(NowFunc()) @@ -337,7 +337,7 @@ func (scope *Scope) Set(name string, value interface{}) *Scope { return scope } -// Get get value by name +// Get get setting by name func (scope *Scope) Get(name string) (interface{}, bool) { return scope.db.Get(name) } @@ -350,12 +350,12 @@ func (scope *Scope) InstanceID() string { return scope.instanceID } -// InstanceSet set value for current instance, but not for associations +// InstanceSet set instance setting for current operation, but not for operations in callbacks, like saving associations callback func (scope *Scope) InstanceSet(name string, value interface{}) *Scope { return scope.Set(name+scope.InstanceID(), value) } -// InstanceGet get setting from current instance +// InstanceGet get instance setting from current operation func (scope *Scope) InstanceGet(name string) (interface{}, bool) { return scope.Get(name + scope.InstanceID()) } @@ -371,7 +371,7 @@ func (scope *Scope) Begin() *Scope { return scope } -// CommitOrRollback commit current transaction if there is no error, otherwise rollback it +// CommitOrRollback commit current transaction if no error happened, otherwise will rollback it func (scope *Scope) CommitOrRollback() *Scope { if _, ok := scope.InstanceGet("gorm:started_transaction"); ok { if db, ok := scope.db.db.(sqlTx); ok { @@ -386,7 +386,7 @@ func (scope *Scope) CommitOrRollback() *Scope { return scope } -// SelectAttrs retur nselected attributes +// SelectAttrs return selected attributes func (scope *Scope) SelectAttrs() []string { if scope.selectAttrs == nil { attrs := []string{} diff --git a/scope_private.go b/scope_private.go index 9309b6f4..c491cc7a 100644 --- a/scope_private.go +++ b/scope_private.go @@ -569,13 +569,6 @@ func (scope *Scope) dropTable() *Scope { return scope } -func (scope *Scope) dropTableIfExists() *Scope { - if scope.Dialect().HasTable(scope.TableName()) { - scope.dropTable() - } - return scope -} - func (scope *Scope) modifyColumn(column string, typ string) { scope.Raw(fmt.Sprintf("ALTER TABLE %v MODIFY %v %v", scope.QuotedTableName(), scope.Quote(column), typ)).Exec() } diff --git a/utils.go b/utils.go index 4ac2ab10..af11f5d2 100644 --- a/utils.go +++ b/utils.go @@ -14,10 +14,10 @@ import ( // NowFunc returns current time, this function is exported in order to be able // to give the flexibility to the developer to customize it according to their -// needs -// -// e.g: return time.Now().UTC() -// +// needs, e.g: +// gorm.NowFunc = func() time.Time { +// return time.Now().UTC() +// } var NowFunc = func() time.Time { return time.Now() } @@ -116,7 +116,7 @@ type expr struct { args []interface{} } -// Expr generate raw SQL expression for SQL, for example: +// Expr generate raw SQL expression, for example: // DB.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100)) func Expr(expression string, args ...interface{}) *expr { return &expr{expr: expression, args: args} From 89e8d4f5e16442fc42b3652e161f113ad579ce1e Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 7 Mar 2016 21:18:48 +0800 Subject: [PATCH 79/83] Update README --- README.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b3388f19..c3f209c9 100644 --- a/README.md +++ b/README.md @@ -2,33 +2,32 @@ The fantastic ORM library for Golang, aims to be developer friendly. +[![Join the chat at https://gitter.im/jinzhu/gorm](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jinzhu/gorm?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![wercker status](https://app.wercker.com/status/0cb7bb1039e21b74f8274941428e0921/s/master "wercker status")](https://app.wercker.com/project/bykey/0cb7bb1039e21b74f8274941428e0921) [![GoDoc](https://godoc.org/github.com/jinzhu/gorm?status.svg)](https://godoc.org/github.com/jinzhu/gorm) -[![Join the chat at https://gitter.im/jinzhu/gorm](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jinzhu/gorm?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ## Overview * Full-Featured ORM (almost) -* Chainable API -* Auto Migrations -* Relations (Has One, Has Many, Belongs To, Many To Many, [Polymorphism](#polymorphism)) +* Associations (Has One, Has Many, Belongs To, Many To Many, Polymorphism) * Callbacks (Before/After Create/Save/Update/Delete/Find) * Preloading (eager loading) * Transactions -* Embed Anonymous Struct -* Soft Deletes -* Customizable Logger -* Iteration Support via [Rows](#row--rows) +* Composite Primary Key +* SQL Builder +* Auto Migrations +* Logger +* Extendable, write Plugins based on GORM callbacks * Every feature comes with tests * Developer Friendly ## Getting Started -* GORM Guides [jinzhu.github.com/gorm](https://jinzhu.github.com/gorm) +* GORM Guides [jinzhu.github.com/gorm](https://jinzhu.github.io/gorm) ## Upgrading To V1.0 -* [CHANGELOG]() +* [CHANGELOG](https://jinzhu.github.io/gorm/changelog.html) # Author From d169ac55e26793e9624caacd2c3863d630a15118 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 7 Mar 2016 21:24:42 +0800 Subject: [PATCH 80/83] Delete CHANGELOG.md --- CHANGELOG.md | 56 ---------------------------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 949ad386..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,56 +0,0 @@ -# Change Log - -## v1.0 - -#### Breaking Changes - -* **`gorm.Open` return `*gorm.DB` instead of `gorm.DB`** - -* **Updating will only update changed fields** - - Most applications won't be affected, only when you are changing updating values in callbacks like `BeforeSave`, `BeforeUpdate`, you should use `scope.SetColumn` then, for example: - - ```go - func (user *User) BeforeUpdate(scope *gorm.Scope) { - if pw, err := bcrypt.GenerateFromPassword(user.Password, 0); err == nil { - scope.SetColumn("EncryptedPassword", pw) - // user.EncryptedPassword = pw // doesn't work, won't including EncryptedPassword field when updating - } - } - ``` - -* **Soft delete's default querying scope will only check `deleted_at IS NULL`** - - Before `db.Find(&user)` will generate querying SQL if user has `DeletedAt` field - - `SELECT * FROM users WHERE deleted_at IS NULL OR deleted_at <= '0001-01-02'` - - Now won't include blank time check `<= '0001-01-02` anymore, will generat SQL like: - - `SELECT * FROM users WHERE deleted_at IS NULL` - - So your application's `DeletedAt` field should not use `time.Time` as data type, need to use pointer `*time.Time` or something like `NullTime`. - If you are using `gorm.Model`, then you are good, nothing need to be change, just make sure all records using blank time for `deleted_at` has been set to NULL, sample migration script: - - ```go - import ( - "github.com/jinzhu/now" - ) - - func main() { - var models = []interface{}{&User{}, &Image{}} - for _, model := range models { - db.Unscoped().Model(model).Where("deleted_at < ?", now.MustParse("0001-01-02")).Update("deleted_at", gorm.Expr("NULL")) - } - } - ``` - -* **New ToDBName logic** - - Before when GORM convert Struct, Field's name to db name, only those common initialisms from [golint](https://github.com/golang/lint/blob/master/lint.go#L702) like `HTTP`, `URI` are special handled. - - So field `HTTP`'s db name will be `http` not `h_t_t_p`, but some other initialisms like `SKU` that not in golint, it's db name will be `s_k_u`, this release fixed this, any upper case initialisms should be converted correctly. - - If your applications using some upper case initialisms which doesn't exist in [golint](https://github.com/golang/lint/blob/master/lint.go#L702), you need to overwrite generated column name with tag, like `sql:"column:s_k_u"`, or alert your database's column name according to new logic - -* **Builtin `Hstore` struct for postgres has been moved to `github.com/jinzhu/gorm/dialects/postgres`** From 946909f1e8790ca4f9ddddb8b7cb2e974439f5a1 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 7 Mar 2016 21:33:48 +0800 Subject: [PATCH 81/83] Add DropTableIfExists back --- main.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/main.go b/main.go index 8baf1deb..46d64b29 100644 --- a/main.go +++ b/main.go @@ -485,6 +485,17 @@ func (s *DB) DropTable(values ...interface{}) *DB { return db } +// DropTableIfExists drop table if it is exist +func (s *DB) DropTableIfExists(values ...interface{}) *DB { + db := s.clone() + for _, value := range values { + if s.HasTable(value) { + db.AddError(s.DropTable(value).Error) + } + } + return db +} + // HasTable check has table or not func (s *DB) HasTable(value interface{}) bool { var ( From 2c089573cd895e71ea365deb161f535bc70f77e8 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 7 Mar 2016 23:35:31 +0800 Subject: [PATCH 82/83] Add tests for override belogns to foreign keys with tag --- association_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ model_struct.go | 5 ++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/association_test.go b/association_test.go index 8ba198e5..90ad4298 100644 --- a/association_test.go +++ b/association_test.go @@ -5,6 +5,8 @@ import ( "reflect" "sort" "testing" + + "github.com/jinzhu/gorm" ) func TestBelongsTo(t *testing.T) { @@ -177,6 +179,49 @@ func TestBelongsTo(t *testing.T) { } } +func TestBelongsToOverrideForeignKey1(t *testing.T) { + type Profile struct { + gorm.Model + Name string + } + + type User struct { + gorm.Model + Profile Profile `gorm:"ForeignKey:ProfileRefer"` + ProfileRefer int + } + + DB.AutoMigrate(&User{}) + DB.AutoMigrate(&Profile{}) + + var user = User{Model: gorm.Model{ID: 1}, ProfileRefer: 10} + if err := DB.Model(&user).Association("Profile").Find(&[]Profile{}).Error; err != nil { + t.Errorf("Override belongs to foreign key with tag") + } +} + +func TestBelongsToOverrideForeignKey2(t *testing.T) { + type Profile struct { + gorm.Model + Refer string + Name string + } + + type User struct { + gorm.Model + Profile Profile `gorm:"ForeignKey:ProfileID;AssociationForeignKey:Refer"` + ProfileID int + } + + DB.AutoMigrate(&User{}) + DB.AutoMigrate(&Profile{}) + + var user = User{Model: gorm.Model{ID: 1}, ProfileID: 10} + if err := DB.Model(&user).Association("Profile").Find(&[]Profile{}).Error; err != nil { + t.Errorf("Override belongs to foreign key with tag") + } +} + func TestHasOne(t *testing.T) { user := User{ Name: "has one", diff --git a/model_struct.go b/model_struct.go index b0dccf3b..a0d52352 100644 --- a/model_struct.go +++ b/model_struct.go @@ -447,7 +447,10 @@ func (scope *Scope) GetModelStruct() *ModelStruct { if len(associationForeignKeys) == 0 { for _, foreignKey := range foreignKeys { if strings.HasPrefix(foreignKey, field.Name) { - associationForeignKeys = append(associationForeignKeys, strings.TrimPrefix(foreignKey, field.Name)) + associationForeignKey := strings.TrimPrefix(foreignKey, field.Name) + if foreignField := getForeignField(associationForeignKey, toFields); foreignField != nil { + associationForeignKeys = append(associationForeignKeys, associationForeignKey) + } } } if len(associationForeignKeys) == 0 && len(foreignKeys) == 1 { From 2e9d5e6f761aabeabc443d7dde4250a64b3b7a5c Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Mon, 7 Mar 2016 23:51:04 +0800 Subject: [PATCH 83/83] Test overwrite foreign keys --- association_test.go | 110 +++++++++++++++++++++++++++++++++++++++----- model_struct.go | 10 +++- 2 files changed, 106 insertions(+), 14 deletions(-) diff --git a/association_test.go b/association_test.go index 90ad4298..52d2303f 100644 --- a/association_test.go +++ b/association_test.go @@ -191,12 +191,12 @@ func TestBelongsToOverrideForeignKey1(t *testing.T) { ProfileRefer int } - DB.AutoMigrate(&User{}) - DB.AutoMigrate(&Profile{}) - - var user = User{Model: gorm.Model{ID: 1}, ProfileRefer: 10} - if err := DB.Model(&user).Association("Profile").Find(&[]Profile{}).Error; err != nil { - t.Errorf("Override belongs to foreign key with tag") + if relation, ok := DB.NewScope(&User{}).FieldByName("Profile"); ok { + if relation.Relationship.Kind != "belongs_to" || + !reflect.DeepEqual(relation.Relationship.ForeignFieldNames, []string{"ProfileRefer"}) || + !reflect.DeepEqual(relation.Relationship.AssociationForeignFieldNames, []string{"ID"}) { + t.Errorf("Override belongs to foreign key with tag") + } } } @@ -213,12 +213,12 @@ func TestBelongsToOverrideForeignKey2(t *testing.T) { ProfileID int } - DB.AutoMigrate(&User{}) - DB.AutoMigrate(&Profile{}) - - var user = User{Model: gorm.Model{ID: 1}, ProfileID: 10} - if err := DB.Model(&user).Association("Profile").Find(&[]Profile{}).Error; err != nil { - t.Errorf("Override belongs to foreign key with tag") + if relation, ok := DB.NewScope(&User{}).FieldByName("Profile"); ok { + if relation.Relationship.Kind != "belongs_to" || + !reflect.DeepEqual(relation.Relationship.ForeignFieldNames, []string{"ProfileID"}) || + !reflect.DeepEqual(relation.Relationship.AssociationForeignFieldNames, []string{"Refer"}) { + t.Errorf("Override belongs to foreign key with tag") + } } } @@ -368,6 +368,49 @@ func TestHasOne(t *testing.T) { } } +func TestHasOneOverrideForeignKey1(t *testing.T) { + type Profile struct { + gorm.Model + Name string + UserRefer uint + } + + type User struct { + gorm.Model + Profile Profile `gorm:"ForeignKey:UserRefer"` + } + + if relation, ok := DB.NewScope(&User{}).FieldByName("Profile"); ok { + if relation.Relationship.Kind != "has_one" || + !reflect.DeepEqual(relation.Relationship.ForeignFieldNames, []string{"UserRefer"}) || + !reflect.DeepEqual(relation.Relationship.AssociationForeignFieldNames, []string{"ID"}) { + t.Errorf("Override belongs to foreign key with tag") + } + } +} + +func TestHasOneOverrideForeignKey2(t *testing.T) { + type Profile struct { + gorm.Model + Name string + UserID uint + } + + type User struct { + gorm.Model + Refer string + Profile Profile `gorm:"ForeignKey:UserID;AssociationForeignKey:Refer"` + } + + if relation, ok := DB.NewScope(&User{}).FieldByName("Profile"); ok { + if relation.Relationship.Kind != "has_one" || + !reflect.DeepEqual(relation.Relationship.ForeignFieldNames, []string{"UserID"}) || + !reflect.DeepEqual(relation.Relationship.AssociationForeignFieldNames, []string{"Refer"}) { + t.Errorf("Override belongs to foreign key with tag") + } + } +} + func TestHasMany(t *testing.T) { post := Post{ Title: "post has many", @@ -507,6 +550,49 @@ func TestHasMany(t *testing.T) { } } +func TestHasManyOverrideForeignKey1(t *testing.T) { + type Profile struct { + gorm.Model + Name string + UserRefer uint + } + + type User struct { + gorm.Model + Profile []Profile `gorm:"ForeignKey:UserRefer"` + } + + if relation, ok := DB.NewScope(&User{}).FieldByName("Profile"); ok { + if relation.Relationship.Kind != "has_many" || + !reflect.DeepEqual(relation.Relationship.ForeignFieldNames, []string{"UserRefer"}) || + !reflect.DeepEqual(relation.Relationship.AssociationForeignFieldNames, []string{"ID"}) { + t.Errorf("Override belongs to foreign key with tag") + } + } +} + +func TestHasManyOverrideForeignKey2(t *testing.T) { + type Profile struct { + gorm.Model + Name string + UserID uint + } + + type User struct { + gorm.Model + Refer string + Profile []Profile `gorm:"ForeignKey:UserID;AssociationForeignKey:Refer"` + } + + if relation, ok := DB.NewScope(&User{}).FieldByName("Profile"); ok { + if relation.Relationship.Kind != "has_many" || + !reflect.DeepEqual(relation.Relationship.ForeignFieldNames, []string{"UserID"}) || + !reflect.DeepEqual(relation.Relationship.AssociationForeignFieldNames, []string{"Refer"}) { + t.Errorf("Override belongs to foreign key with tag") + } + } +} + func TestManyToMany(t *testing.T) { DB.Raw("delete from languages") var languages = []Language{{Name: "ZH"}, {Name: "EN"}} diff --git a/model_struct.go b/model_struct.go index a0d52352..6df615d1 100644 --- a/model_struct.go +++ b/model_struct.go @@ -298,7 +298,10 @@ func (scope *Scope) GetModelStruct() *ModelStruct { if len(associationForeignKeys) == 0 { for _, foreignKey := range foreignKeys { if strings.HasPrefix(foreignKey, associationType) { - associationForeignKeys = append(associationForeignKeys, strings.TrimPrefix(foreignKey, associationType)) + associationForeignKey := strings.TrimPrefix(foreignKey, associationType) + if foreignField := getForeignField(associationForeignKey, modelStruct.StructFields); foreignField != nil { + associationForeignKeys = append(associationForeignKeys, associationForeignKey) + } } } if len(associationForeignKeys) == 0 && len(foreignKeys) == 1 { @@ -391,7 +394,10 @@ func (scope *Scope) GetModelStruct() *ModelStruct { if len(associationForeignKeys) == 0 { for _, foreignKey := range foreignKeys { if strings.HasPrefix(foreignKey, associationType) { - associationForeignKeys = append(associationForeignKeys, strings.TrimPrefix(foreignKey, associationType)) + associationForeignKey := strings.TrimPrefix(foreignKey, associationType) + if foreignField := getForeignField(associationForeignKey, modelStruct.StructFields); foreignField != nil { + associationForeignKeys = append(associationForeignKeys, associationForeignKey) + } } } if len(associationForeignKeys) == 0 && len(foreignKeys) == 1 {