Gorm is an ORM framework for Golang, which provides encapsulation of database operations and is quite convenient to use.

However, in the development of the project, as more code was written, it was found that there was still room for re-encapsulation on top of it, such as adding error logs, or some frequently used conditional query on a single table, paging query, data update, etc. Moreover, gorM also provides a variety of implementation methods for the same function operation, which is somewhat confused about the new learning, and do not know which one to use.

Therefore, BASED on my own experience and coding habits in the project, I made the following extensions for your reference.

To prepare

In order to be compatible with gorM usage, I use embedded types to extend. The definition is as follows:

type DBExtension struct {
	*gorm.DB
	logger DBLogger
}
Copy the code

The Wrapper object defined in this way is a minimally invasive extension that can point directly to gorM’s original methods as well as to the extended methods.

new

For new data, I recommend using the Save method, which inserts a new piece of data when the data matching the primary key does not exist, and updates all fields when the data matching the primary key does exist. Again, it updates all fields!

Whether the field has been modified or is the default value for the defined type.

Note again: Whether the default values are in effect or not is handled differently in different methods of GORM, and you need to be very careful.

For example, if you define a User structure that has an Age field of type int. (Note: this structure is defined by default in future examples.)

type User struct {
	Id           int     `gorm:"column:id; type:int(11); primary_key"`
	Name         string  `gorm:"column:name; type:varchar(32);" `
	Age          int     `gorm:"column:age; type:int(11);" `
	Description  string  `gorm:"column:description; type:varchar(512);" `
}

func (User) TableName(a) string {
	return "test.user"
}
Copy the code

++ Pay special attention to primary_key in the Id definition, as the Save method will not work without it. ++

If Age is not assigned at the time of definition, the Age of the data will be set to 0.

For new data, this may not be a problem, but for data updates, this is a subtle bug!

So if the Save method has such a hole, why use it?

In short, the decision not to show it is to add and update data, which makes the code more concise and does more good than harm, right?

The extension code is as follows, adding some error judgments and logging:

type TableNameAble interface {
	TableName() string
}

// Update All Fields
func (dw *DBExtension) SaveOne(value TableNameAble) error {
	tableNameAble, ok := value.(TableNameAble)
	if! ok {return errors.New("value doesn't implement TableNameAble")}var err error
	iferr = dw.Save(value).Error; err ! =nil {
		dw.logger.LogErrorc("mysql", err, fmt.Sprintf("Failed to save %s, the value is %+v", tableNameAble.TableName(), value))
	}
	return err
}
Copy the code

Use the following code:


user1 := User{Id:1, Name:"Jeremy", Age: 30, Description: "A gopher"}

iferr := dw.SaveOne(&instInfo); err ! =nil{
    // error handling
    return err
}

Copy the code

When the record does not exist, the Sql statement executed is:

insert into test.user(id ,name, age, description) values(1, "Jeremy", 30, "A gopher")
Copy the code

When the record exists, the statement executed is:

update test.user set name = "Jeremy", age = 30, description = "A gohper" where id = 1
Copy the code

Write new, but also take into account the full field update situation, is not a kill two birds with one stone?

PS: If the primary key Id is an auto-increment column, you do not need to assign a value to the Id when creating a new column. When data is successfully inserted, the Id of the data is automatically updated to the Id field, which is particularly useful in some scenarios.

update

The SaveOne method is a full update, but in most cases, it may only update a partial field of a particular piece of data, or it may only update the changed field. Gorm provides many ways to do this, but it’s also the most confusing.

In this scenario, I usually use two methods. One is to define a special structure, such as:

type UserDesc struct {
	Id           int     `gorm:"column:id; type:int(11); primary_key"`
	Description  string  `gorm:"column:description; type:varchar(512);" `
}

func (UserDesc) TableName(a) string {
	return "test.user"
}

Copy the code

At this point, you can use the SaveOne method to update as follows:


userDesc := UserDesc{Id:1,  Description: "A programmer"}

iferr := dw.SaveOne(&userDesc); err ! =nil{
    // error handling
    return err
}

Copy the code

The SQL statement executed is:

update test.user set description = "A programmer" where id = 1
Copy the code

But most of the time, it is the matching data that wants to be updated according to the matching condition, and SaveOne cannot meet this requirement. So, I made the following extension:

const table_name =  "$Table_Name$"

type UpdateAttrs map[string]interface{}

func NewUpdateAttrs(tableName string) UpdateAttrs  {
	attrMap := make(map[string]interface{})
	attrMap[table_name] = tableName
	return attrMap
}

// Update selected Fields, if attrs is an object, it will ignore default value field; if attrs is map, it will ignore unchanged field.
func (dw *DBExtension) Update(attrs interface{}, query interface{}, args ...interface{}) error {
	var (
		tableNameAble TableNameAble
		ok            bool
		tableName     string
	)

	if tableNameAble, ok = query.(TableNameAble); ok {
		tableName = tableNameAble.TableName()
	}else if tableNameAble, ok = attrs.(TableNameAble); ok {
		tableName = tableNameAble.TableName()
	} else if attrMap, isUpdateAttrs := attrs.(UpdateAttrs); isUpdateAttrs {
		tableName = attrMap[table_name].(string)
		delete(attrMap, table_name)
	}

	if tableName == "" {
		return errors.New("can't get table name from both attrs and query")}varerr error db := dw.Table(tableName).Where(query, args...) .Update(attrs)iferr = db.Error; err ! =nil {
		dw.logger.LogErrorc("mysql", err, fmt.Sprintf("failed to update %s, query is %+v, args are %+v, attrs is %+v", tableName, query, args, attrs))
	}

	if db.RowsAffected == 0 {
		dw.logger.LogWarnc("mysql".nil, fmt.Sprintf("No rows is updated.For %s, query is %+v, args are %+v, attrs is %+v", tableName, query, args, attrs))
	}

	return err
}
Copy the code

Below, I will explain how to use them one by one with Sql statements.

Let’s start with the following statement:

update test.user set description = "A programmer" where id = 1
Copy the code

Now, there are several ways to do this

  • Writing a
udateAttrs := User{Description: "A programmer"}
condition := User{Id: 1}
iferr := dw.Update(&udateAttrs, condition); err ! = nil{ // error handlingreturn err
}
Copy the code
  • Write two
udateAttrs := User{Description: "A programmer"}
if err := dw.Update(&udateAttrs, "id = ?", 1); err ! = nil{ // error handlingreturn err
}
Copy the code
  • Write three
udateAttrs := NewUpdateAttrs("test.user")
udateAttrs["description"] = "A programmer"

if err := dw.Update(&udateAttrs, "id = ?", 1); err ! = nil{ // error handlingreturn err
}
Copy the code
  • Write four
udateAttrs := NewUpdateAttrs("test.user")
udateAttrs["description"] = "A programmer"
condition := User{Id: 1}

iferr := dw.Update(&udateAttrs, condition); err ! = nil{ // error handlingreturn err
}
Copy the code

At first glance, the four ways are similar. So why all these different ways of writing it?

This is not to show off the different ways of writing the return, but because gorM’s native Update method ignores default values for struct arguments.

For example, if you want to empty descritpion, write something like this:

udateAttrs := User{Description: ""}
condition := User{Id: 1}
iferr := dw.Update(&udateAttrs, condition); err ! = nil{ // error handlingreturn err
}
Copy the code

Descritpion is not going to be updated, so you have to write method three or method four, for example

udateAttrs := NewUpdateAttrs("test.user")
udateAttrs["description"] = ""
condition := User{Id: 1}
iferr := dw.Update(&udateAttrs, condition); err ! = nil{ // error handlingreturn err
}
Copy the code

To be fulfilled:

update test.user set description = "" where id = 1
Copy the code

The power of writing method 2 (3) lies in that it can specify matching conditions more freely, for example:

udateAttrs := User{Description: "A programmer"}
if err := dw.Update(&udateAttrs, "id in (?) and age > ? and description != ?", int []} {1, 2, 30,""); err ! = nil{ // error handlingreturn err
}
Copy the code

SQL to execute:

update test.user set description = "A programmer" where id inAnd age > 30 and description! =' '
Copy the code

To be continued…

Code address: Github:Ksloveyuan/ Gorm-ex