github.com/angenalZZZ/gofunc@v0.0.0-20210507121333-48ff1be3917b/data/bulk/gorm-bulk/bulk_insert.go (about) 1 package gormbulk 2 3 import ( 4 "errors" 5 "fmt" 6 "github.com/angenalZZZ/gofunc/f" 7 "reflect" 8 "strings" 9 "time" 10 11 // https://gorm.io/zh_CN/docs 12 "github.com/jinzhu/gorm" 13 ) 14 15 // BulkInsert executes the query to insert multiple records at once. 16 // [objects] must be a slice of struct. 17 // 18 // [chunkSize] is a number of variables embedded in query. To prevent the error which occurs embedding a large number of variables at once 19 // and exceeds the limit of prepared statement. Larger size normally leads to better performance, in most cases 2000 to 3000 is reasonable. 20 // 21 // [excludeColumns] is column names to exclude from insert. 22 func BulkInsert(db *gorm.DB, objects []interface{}, chunkSize int, interval time.Duration, excludeColumns ...string) error { 23 // Split records with specified size not to exceed Database parameter limit 24 for _, objSet := range f.SplitObjects(objects, chunkSize) { 25 if err := insertObjSet(db, objSet, excludeColumns...); err != nil { 26 return err 27 } 28 if interval > 0 { 29 time.Sleep(interval) 30 } 31 } 32 return nil 33 } 34 35 func insertObjSet(db *gorm.DB, objects []interface{}, excludeColumns ...string) error { 36 if len(objects) == 0 { 37 return nil 38 } 39 40 firstAttrs, err := extractMapValue(objects[0], excludeColumns) 41 if err != nil { 42 return err 43 } 44 45 attrSize := len(firstAttrs) 46 47 // Scope to eventually run SQL 48 mainScope := db.NewScope(objects[0]) 49 // Store placeholders for embedding variables 50 placeholders := make([]string, 0, attrSize) 51 52 // Replace with database column name 53 dbColumns := make([]string, 0, attrSize) 54 for _, key := range f.MapKeySorted(firstAttrs) { 55 dbColumns = append(dbColumns, mainScope.Quote(key)) 56 } 57 58 for _, obj := range objects { 59 objAttrs, err := extractMapValue(obj, excludeColumns) 60 if err != nil { 61 return err 62 } 63 64 // If object sizes are different, SQL statement loses consistency 65 if len(objAttrs) != attrSize { 66 return errors.New("attribute sizes are inconsistent") 67 } 68 69 scope := db.NewScope(obj) 70 71 // Append variables 72 variables := make([]string, 0, attrSize) 73 for _, key := range f.MapKeySorted(objAttrs) { 74 scope.AddToVars(objAttrs[key]) 75 variables = append(variables, "?") 76 } 77 78 valueQuery := "(" + strings.Join(variables, ", ") + ")" 79 placeholders = append(placeholders, valueQuery) 80 81 // Also append variables to mainScope 82 mainScope.SQLVars = append(mainScope.SQLVars, scope.SQLVars...) 83 } 84 85 insertOption := "" 86 if val, ok := db.Get("gorm:insert_option"); ok { 87 strVal, ok := val.(string) 88 if !ok { 89 return errors.New("gorm:insert_option should be a string") 90 } 91 insertOption = strVal 92 } 93 94 mainScope.Raw(fmt.Sprintf("INSERT INTO %s (%s) VALUES %s %s", 95 mainScope.QuotedTableName(), 96 strings.Join(dbColumns, ", "), 97 strings.Join(placeholders, ", "), 98 insertOption, 99 )) 100 101 return db.Exec(mainScope.SQL, mainScope.SQLVars...).Error 102 } 103 104 // Obtain columns and values required for insert from interface 105 func extractMapValue(value interface{}, excludeColumns []string) (map[string]interface{}, error) { 106 rv := reflect.ValueOf(value) 107 if rv.Kind() == reflect.Ptr { 108 rv = rv.Elem() 109 value = rv.Interface() 110 } 111 if rv.Kind() != reflect.Struct { 112 return nil, errors.New("value must be kind of Struct") 113 } 114 115 var attrs = map[string]interface{}{} 116 117 for _, field := range (&gorm.Scope{Value: value}).Fields() { 118 // Exclude relational record because it's not directly contained in database columns 119 _, hasForeignKey := field.TagSettingsGet("FOREIGNKEY") 120 121 if !f.StringsContains(excludeColumns, field.Struct.Name) && field.StructField.Relationship == nil && !hasForeignKey && 122 !field.IsIgnored && !fieldIsAutoIncrement(field) && !fieldIsPrimaryAndBlank(field) { 123 if (field.Struct.Name == "CreatedAt" || field.Struct.Name == "UpdatedAt") && field.IsBlank { 124 attrs[field.DBName] = time.Now() 125 } else if field.StructField.HasDefaultValue && field.IsBlank { 126 // If default value presents and field is empty, assign a default value 127 if val, ok := field.TagSettingsGet("DEFAULT"); ok { 128 attrs[field.DBName] = val 129 } else { 130 attrs[field.DBName] = field.Field.Interface() 131 } 132 } else { 133 attrs[field.DBName] = field.Field.Interface() 134 } 135 } 136 } 137 return attrs, nil 138 } 139 140 func fieldIsAutoIncrement(field *gorm.Field) bool { 141 if value, ok := field.TagSettingsGet("AUTO_INCREMENT"); ok { 142 return strings.ToLower(value) != "false" 143 } 144 return false 145 } 146 147 func fieldIsPrimaryAndBlank(field *gorm.Field) bool { 148 return field.IsPrimaryKey && field.IsBlank 149 }