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  }