返回顶部
首页 > 资讯 > 精选 >基于GORM如何实现CreateOrUpdate
  • 498
分享到

基于GORM如何实现CreateOrUpdate

2023-07-04 10:07:21 498人浏览 独家记忆
摘要

这篇文章主要讲解了“基于GORM如何实现CreateOrUpdate”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“基于GoRM如何实现CreateOrUpdate”吧!GORM 写

这篇文章主要讲解了“基于GORM如何实现CreateOrUpdate”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“基于GoRM如何实现CreateOrUpdate”吧!

    GORM 写接口原理

    我们先来看下 GORM 提供了那些方法来支持我们往数据库插入数据,对 GORM 比较熟悉的同学可以忽略这部分:

    Create

    插入一条记录到数据库,注意需要通过数据的指针来创建,回填主键;

    // Create insert the value into databasefunc (db *DB) Create(value interface{}) (tx *DB) {if db.CreateBatchSize > 0 {return db.CreateInBatches(value, db.CreateBatchSize)}tx = db.getInstance()tx.Statement.Dest = valuereturn tx.callbacks.Create().Execute(tx)}

    赋值 Dest 后直接进入 Create 的 callback 流程。

    Save

    保存所有的字段,即使字段是零值。如果我们传入的结构主键为零值,则会插入记录。

    // Save update value in database, if the value doesn't have primary key, will insert itfunc (db *DB) Save(value interface{}) (tx *DB) {tx = db.getInstance()tx.Statement.Dest = valuereflectValue := reflect.Indirect(reflect.ValueOf(value))for reflectValue.Kind() == reflect.Ptr || reflectValue.Kind() == reflect.Interface {reflectValue = reflect.Indirect(reflectValue)}switch reflectValue.Kind() {case reflect.Slice, reflect.Array:if _, ok := tx.Statement.Clauses["ON CONFLICT"]; !ok {tx = tx.Clauses(clause.OnConflict{UpdateAll: true})}tx = tx.callbacks.Create().Execute(tx.Set("gorm:update_track_time", true))case reflect.Struct:if err := tx.Statement.Parse(value); err == nil && tx.Statement.Schema != nil {for _, pf := range tx.Statement.Schema.PrimaryFields {if _, isZero := pf.ValueOf(tx.Statement.Context, reflectValue); isZero {return tx.callbacks.Create().Execute(tx)}}}fallthroughdefault:selectedUpdate := len(tx.Statement.Selects) != 0// when updating, use all fields including those zero-value fieldsif !selectedUpdate {tx.Statement.Selects = append(tx.Statement.Selects, "*")}tx = tx.callbacks.Update().Execute(tx)if tx.Error == nil && tx.RowsAffected == 0 && !tx.DryRun && !selectedUpdate {result := reflect.New(tx.Statement.Schema.ModelType).Interface()if result := tx.Session(&Session{}).Limit(1).Find(result); result.RowsAffected == 0 {return tx.Create(value)}}}return}

    关注点:

    • 在 reflect.Struct 的分支,判断 PrimaryFields 也就是主键列是否为零值,如果是,直接开始调用 Create 的 callback,这也和 Save 的说明匹配;

    • switch 里面用到了 fallthrough 关键字,说明 switch 命中后继续往下命中 default;

    • 如果我们没有用 Select() 方法指定需要更新的字段,则默认是全部更新,包含所有零值字段,这里用的通配符 *

    • 如果主键不为零值,说明记录已经存在,这个时候就会去更新。

    事实上有一些业务场景下,我们可以用 Save 来实现 CreateOrUpdate 的语义:

    • 首次调用时主键ID为空,这时 Save 会走到 Create 分支去插入数据。

    • 随后调用时存在主键ID,触发更新逻辑。

    但 Save 本身语义其实比较混乱,不太建议使用,把这部分留给业务自己实现,用Updates,Create用起来更明确些。

    Update & Updates

    Update 前者更新单个列。

    Updates 更新多列,且当使用 struct 更新时,默认情况下,GORM 只会更新非零值的字段(可以用 Select 指定来解这个问题)。使用 map 更新时则会全部更新。

    // Update update attributes with callbacks, refer: https://gorm.io/docs/update.html#Update-Changed-Fieldsfunc (db *DB) Update(column string, value interface{}) (tx *DB) {tx = db.getInstance()tx.Statement.Dest = map[string]interface{}{column: value}return tx.callbacks.Update().Execute(tx)}// Updates update attributes with callbacks, refer: Https://gorm.io/docs/update.html#Update-Changed-Fieldsfunc (db *DB) Updates(values interface{}) (tx *DB) {tx = db.getInstance()tx.Statement.Dest = valuesreturn tx.callbacks.Update().Execute(tx)}

    这里也能从实现中看出来一些端倪。Update 接口内部是封装了一个 map[string]interface{},而 Updates 则是可以接受 map 也可以走 struct,最终写入 Dest。

    FirstOrInit

    获取第一条匹配的记录,或者根据给定的条件初始化一个实例(仅支持 struct 和 map)

    // FirstOrInit gets the first matched record or initialize a new instance with given conditions (only works with struct or map conditions)func (db *DB) FirstOrInit(dest interface{}, conds ...interface{}) (tx *DB) {queryTx := db.Limit(1).Order(clause.OrderByColumn{Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey},})if tx = queryTx.Find(dest, conds...); tx.RowsAffected == 0 {if c, ok := tx.Statement.Clauses["WHERE"]; ok {if where, ok := c.Expression.(clause.Where); ok {tx.assignInterfacesToValue(where.Exprs)}}// initialize with attrs, condsif len(tx.Statement.attrs) > 0 {tx.assignInterfacesToValue(tx.Statement.attrs...)}}// initialize with attrs, condsif len(tx.Statement.assigns) > 0 {tx.assignInterfacesToValue(tx.Statement.assigns...)}return}

    注意,Init 和 Create 的区别,如果没有找到,这里会把实例给初始化,不会存入 DB,可以看到 RowsAffected == 0 分支的处理,这里并不会走 Create 的 callback 函数。这里的定位是一个纯粹的读接口。

    FirstOrCreate

    获取第一条匹配的记录,或者根据给定的条件创建一条新纪录(仅支持 struct 和 map 条件)。FirstOrCreate可能会执行两条sql,他们是一个事务中的。

    // FirstOrCreate gets the first matched record or create a new one with given conditions (only works with struct, map conditions)func (db *DB) FirstOrCreate(dest interface{}, conds ...interface{}) (tx *DB) {tx = db.getInstance()queryTx := db.Session(&Session{}).Limit(1).Order(clause.OrderByColumn{Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey},})if result := queryTx.Find(dest, conds...); result.Error == nil {if result.RowsAffected == 0 {if c, ok := result.Statement.Clauses["WHERE"]; ok {if where, ok := c.Expression.(clause.Where); ok {result.assignInterfacesToValue(where.Exprs)}}// initialize with attrs, condsif len(db.Statement.attrs) > 0 {result.assignInterfacesToValue(db.Statement.attrs...)}// initialize with attrs, condsif len(db.Statement.assigns) > 0 {result.assignInterfacesToValue(db.Statement.assigns...)}return tx.Create(dest)} else if len(db.Statement.assigns) > 0 {exprs := tx.Statement.BuildCondition(db.Statement.assigns[0], db.Statement.assigns[1:]...)assigns := map[string]interface{}{}for _, expr := range exprs {if eq, ok := expr.(clause.Eq); ok {switch column := eq.Column.(type) {case string:assigns[column] = eq.Valuecase clause.Column:assigns[column.Name] = eq.Valuedefault:}}}return tx.Model(dest).Updates(assigns)}} else {tx.Error = result.Error}return tx}

    注意区别,同样是构造 queryTx 去调用 Find 方法查询,后续的处理很关键:

    • 若没有查到结果,将 where 条件,Attrs() 以及 Assign() 方法赋值的属性写入对象,从源码可以看到是通过三次 assignInterfacesToValue 实现的。属性更新后,调用 Create 方法往数据库中插入;

    • 若查到了结果,但 Assign() 此前已经写入了一些属性,就将其写入对象,进行 Updates 调用。

    第一个分支好理解,需要插入新数据。重点在于 else if len(db.Statement.assigns) > 0 分支。

    我们调用 FirstOrCreate 时,需要传入一个对象,再传入一批条件,这批条件会作为 Where 语句的部分在一开始进行查询。而这个函数同时可以配合 Assign() 使用,这一点就赋予了生命力。

    不管是否找到记录,Assign 都会将属性赋值给 struct,并将结果写回数据库。

    方案一:FirstOrCreate + Assign

    func (db *DB) Attrs(attrs ...interface{}) (tx *DB) {tx = db.getInstance()tx.Statement.attrs = attrsreturn}func (db *DB) Assign(attrs ...interface{}) (tx *DB) {tx = db.getInstance()tx.Statement.assigns = attrsreturn}

    这种方式充分利用了 Assign 的能力。我们在上面 FirstOrCreate 的分析中可以看出,这里是会将 Assign 进来的属性应用到 struct 上,写入数据库的。区别只在于是插入(Insert)还是更新(Update)。

    // 未找到 user,根据条件和 Assign 属性创建记录db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrCreate(&user)// SELECT * FROM users WHERE name = 'non_existing' ORDER BY id LIMIT 1;// INSERT INTO "users" (name, age) VALUES ("non_existing", 20);// user -> User{ID: 112, Name: "non_existing", Age: 20}// 找到了 `name` = `jinzhu` 的 user,依然会根据 Assign 更新记录db.Where(User{Name: "jinzhu"}).Assign(User{Age: 20}).FirstOrCreate(&user)// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;// UPDATE users SET age=20 WHERE id = 111;// user -> User{ID: 111, Name: "jinzhu", Age: 20}

    所以,要实现 CreateOrUpdate,我们可以将需要 Update 的属性通过 Assign 函数放进来,随后如果通过 Where 找到了记录,也会将 Assign 属性应用上,随后 Update。

    这样的思路一定是可以跑通的,但使用之前要看场景。

    为什么?

    因为参看上面源码我们就知道,FirstOrCreate 本质是 Select + Insert 或者 Select + Update。

    无论怎样,都是两条 SQL,可能有并发安全问题。如果你的业务场景不存在并发,可以放心用 FirstOrCreate + Assign,功能更多,适配更多场景。

    而如果可能有并发安全的坑,我们就要考虑方案二:Upsert。

    方案二:Upsert

    鉴于 MySQL 提供了 ON DUPLICATE KEY UPDATE 的能力,我们可以充分利用唯一键的约束,来搞定并发场景下的 CreateOrUpdate。

    import "gorm.io/gorm/clause"// 不处理冲突DB.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)// `id` 冲突时,将字段值更新为默认值DB.Clauses(clause.OnConflict{  Columns:   []clause.Column{{Name: "id"}},  DoUpdates: clause.Assignments(map[string]interface{}{"role": "user"}),}).Create(&users)// MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET ***; SQL Server// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE ***; Mysql// Update columns to new value on `id` conflictDB.Clauses(clause.OnConflict{  Columns:   []clause.Column{{Name: "id"}},  DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),}).Create(&users)// MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET "name"="excluded"."name"; SQL Server// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age"; postgresql// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age=VALUES(age); mysql

    这里依赖了 GORM 的 Clauses 方法,我们来看一下:

    type Interface interface {      Name() string      Build(Builder)      MergeClause(*Clause)  }// AddClause add clausefunc (stmt *Statement) AddClause(v clause.Interface) {if optimizer, ok := v.(StatementModifier); ok {optimizer.ModifyStatement(stmt)} else {name := v.Name()c := stmt.Clauses[name]c.Name = namev.MergeClause(&c)stmt.Clauses[name] = c}}

    这里添加进来一个 Clause 之后,会调用 MergeClause 将语句进行合并,而 OnConflict 的适配是这样:

    package clausetype OnConflict struct {Columns      []ColumnWhere        WhereTargetWhere  WhereOnConstraint stringDoNothing    boolDoUpdates    SetUpdateAll    bool}func (OnConflict) Name() string {return "ON CONFLICT"}// Build build onConflict clausefunc (onConflict OnConflict) Build(builder Builder) {if len(onConflict.Columns) > 0 {builder.WriteByte('(')for idx, column := range onConflict.Columns {if idx > 0 {builder.WriteByte(',')}builder.WriteQuoted(column)}builder.WriteString(`) `)}if len(onConflict.TargetWhere.Exprs) > 0 {builder.WriteString(" WHERE ")onConflict.TargetWhere.Build(builder)builder.WriteByte(' ')}if onConflict.OnConstraint != "" {builder.WriteString("ON CONSTRAINT ")builder.WriteString(onConflict.OnConstraint)builder.WriteByte(' ')}if onConflict.DoNothing {builder.WriteString("DO NOTHING")} else {builder.WriteString("DO UPDATE SET ")onConflict.DoUpdates.Build(builder)}if len(onConflict.Where.Exprs) > 0 {builder.WriteString(" WHERE ")onConflict.Where.Build(builder)builder.WriteByte(' ')}}// MergeClause merge onConflict clausesfunc (onConflict OnConflict) MergeClause(clause *Clause) {clause.Expression = onConflict}

    初阶的用法中,我们只需要关注三个属性:

    • DoNothing:冲突后不处理,参照上面的 Build 实现可以看到,这里只会加入 DO NOTHING;

    • DoUpdates: 配置一批需要赋值的 KV,如果没有指定 DoNothing,会根据这一批 Assignment 来写入要更新的列和值;

    type Set []Assignmenttype Assignment struct {Column ColumnValue  interface{}}
    • UpdateAll: 冲突后更新所有的值(非 default tag字段)。

    需要注意的是,所谓 OnConflict,并不一定是主键冲突,唯一键也包含在内。所以,使用 OnConflict 这套 Upsert 的先决条件是【唯一索引】或【主键】都可以。生成一条SQL语句,并发安全。

    如果没有唯一索引的限制,我们就无法复用这个能力,需要考虑别的解法。

    感谢各位的阅读,以上就是“基于GORM如何实现CreateOrUpdate”的内容了,经过本文的学习后,相信大家对基于GORM如何实现CreateOrUpdate这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是编程网,小编将为大家推送更多相关知识点的文章,欢迎关注!

    --结束END--

    本文标题: 基于GORM如何实现CreateOrUpdate

    本文链接: https://lsjlt.com/news/344120.html(转载时请注明来源链接)

    有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

    猜你喜欢
    • 基于GORM如何实现CreateOrUpdate
      这篇文章主要讲解了“基于GORM如何实现CreateOrUpdate”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“基于GORM如何实现CreateOrUpdate”吧!GORM 写...
      99+
      2023-07-04
    • 基于GORM实现CreateOrUpdate方法详解
      目录正文GORM 写接口原理CreateSaveUpdate & UpdatesFirstOrInitFirstOrCreate方案一:FirstOrCreate ...
      99+
      2024-04-02
    • 基于PyTorch如何实现EdgeCNN
      这篇文章主要讲解了“基于PyTorch如何实现EdgeCNN”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“基于PyTorch如何实现EdgeCNN”吧!一、导入相关库本项目是采用自己实现的E...
      99+
      2023-07-05
    • Spring如何基于XML实现Aop
      本篇内容介绍了“Spring如何基于XML实现Aop”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!目录项目结构具体步骤创建maven 项目 ...
      99+
      2023-06-20
    • 基于webman的GraphQL如何实现
      这篇“基于webman的GraphQL如何实现”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“基于webman的GraphQL...
      99+
      2023-07-05
    • 基于Ehcache如何实现Spring缓存
      这篇文章主要介绍了基于Ehcache如何实现Spring缓存,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。一 简介缓存,通过将数据保存在缓冲...
      99+
      2024-04-02
    • 如何实现基于HBase的报表
      小编给大家分享一下如何实现基于HBase的报表,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!   ...
      99+
      2024-04-02
    • 基于Node.js如何实现WebSocket通信
      这篇文章将为大家详细讲解有关基于Node.js如何实现WebSocket通信,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。node的依赖包node中实现Websocket...
      99+
      2024-04-02
    • 基于vue如何实现tree插件
      这篇文章将为大家详细讲解有关基于vue如何实现tree插件,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。iview提供的控件iview已经很成熟了,如果说我写的控件和iv...
      99+
      2024-04-02
    • 基于Python如何实现Hash算法
      本篇内容主要讲解“基于Python如何实现Hash算法”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“基于Python如何实现Hash算法”吧!1 前言Simhash的算法简单的来说就是,从海量文...
      99+
      2023-06-29
    • 基于jQuery的连线如何实现
      这篇文章主要介绍“基于jQuery的连线如何实现”,在日常操作中,相信很多人在基于jQuery的连线如何实现问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”基于jQuery的连线如何实现”的疑惑有所帮助!接下来...
      99+
      2023-07-05
    • Java基于IDEA如何实现http编程
      这篇文章主要介绍了Java基于IDEA如何实现http编程,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。java基本数据类型有哪些Java的基本数据类型分为:1、整数类型,用...
      99+
      2023-06-14
    • 基于Redis如何实现阻塞队列
      这篇文章主要介绍“基于Redis如何实现阻塞队列”,在日常操作中,相信很多人在基于Redis如何实现阻塞队列问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”基于Redis如何实现阻塞队列”的疑惑有所帮助!接下来...
      99+
      2023-06-22
    • vue基于Teleport如何实现Modal组件
      这篇文章将为大家详细讲解有关vue基于Teleport如何实现Modal组件,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。1.认识Teleport像我们如果写Modal组件、Message组件、Loadi...
      99+
      2023-06-15
    • 基于face_recognition如何实现人脸识别
      这篇文章将为大家详细讲解有关基于face_recognition如何实现人脸识别,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。准备工作我们的人脸识别基于face_recognition库。f...
      99+
      2023-06-17
    • 基于layui如何实现登录页面
      本篇内容主要讲解“基于layui如何实现登录页面”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“基于layui如何实现登录页面”吧!首先给看下效果图吧!html、js<!DOCTYPE&nb...
      99+
      2023-06-21
    • 基于Echarts如何实现饼图效果
      这篇文章主要讲解了“基于Echarts如何实现饼图效果”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“基于Echarts如何实现饼图效果”吧!1 显示数值效果 series 下的label 饼...
      99+
      2023-06-30
    • Java基于quasar如何实现协程池
      这篇文章主要介绍了Java基于quasar如何实现协程池,具有一定借鉴价值,需要的朋友可以参考下。下面就和我一起来看看吧。业务场景:golang与swoole都拥抱了协程,在同任务并发数量下,协程可比线程多几倍。所以最近在查询java时了解...
      99+
      2023-07-02
    • 基于Unity3D如何实现仿真时钟
      本篇内容介绍了“基于Unity3D如何实现仿真时钟”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!时钟小工具开发1、搭建UI新建一个Image...
      99+
      2023-07-05
    • 如何基于solr实现全文检索
      这篇文章将为大家详细讲解有关如何基于solr实现全文检索,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。Solr是一个独立的企业级搜索应用服务器,它对外提供类似于Web-service的API接口。用户可以...
      99+
      2023-05-30
      solr
    软考高级职称资格查询
    编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
    • 官方手机版

    • 微信公众号

    • 商务合作