gorm源码解析(四):事务,预编译
文章目录
- 前言
- 事务
- 自己控制事务
- 用 Transaction方法包装事务
- 预编译
- 事务结合预编译
- 总结
前言
前几篇文章介绍gorm的整体设计,增删改查的具体实现流程。本文将聚焦与事务和预编译部分
事务
自己控制事务
用gorm框架,可以自己控制事务的Begin,Commit和Rollback,如下所示:
// 开始事务
tx := db.Begin()// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
tx.Create(...) // ... // 遇到错误时回滚事务
tx.Rollback()// 否则,提交事务
tx.Commit()
下面看看每个api的源码实现
Beigin:
- 新建一个db实例,作为本次事务操作的会话
- 非预编译模式下:调sql.DB的BeginTx方法,让
tx.Statement.ConnPool
持有其返回的sql.Tx- 之后的增删改查操作,都用这个sql.Tx执行,会用同一个db连接(也就是调begin的那个连接)
预编译模式本文后面再介绍,这里只关注非预编译模式
func (db *DB) Begin(opts ...*sql.TxOptions) *DB { var ( // clone statement // 新建的tx.clone = 1 tx = db.getInstance().Session(&Session{Context: db.Statement.Context, NewDB: db.clone == 1}) opt *sql.TxOptions err error ) if len(opts) > 0 { opt = opts[0] } switch beginner := tx.Statement.ConnPool.(type) { // 非预编译模式下: case TxBeginner: tx.Statement.ConnPool, err = beginner.BeginTx(tx.Statement.Context, opt) // 预编译模式下: case ConnPoolBeginner: tx.Statement.ConnPool, err = beginner.BeginTx(tx.Statement.Context, opt) default: err = ErrInvalidTransaction } if err != nil { tx.AddError(err) } return tx
}
Commit和Rollback:调sql.Tx的Commit和Rollback方法,内部会调mysql驱动mysqlTx的Commit和Rollback方法,完成事务的提交和回滚操作
func (db *DB) Commit() *DB { if committer, ok := db.Statement.ConnPool.(TxCommitter); ok && committer != nil && !reflect.ValueOf(committer).IsNil() { db.AddError(committer.Commit()) } else { db.AddError(ErrInvalidTransaction) } return db
} func (db *DB) Rollback() *DB { if committer, ok := db.Statement.ConnPool.(TxCommitter); ok && committer != nil { if !reflect.ValueOf(committer).IsNil() { db.AddError(committer.Rollback()) } } else { db.AddError(ErrInvalidTransaction) } return db
}
用 Transaction方法包装事务
然而,自己控制事务有以下问题:
- 很多重复代码,样板代码
- 自己控制事务生命周期容易出问题,例如可能忘记commit或rollback,或者说发生panic但没有recover,导致没有触发commit或rollback
于是gorm提供了Transaction
方法,帮我们调了Begin,事务执行成功后Commit,事务执行失败或发生panic时执行Rollback操作,也就是帮我们控制事务的生命周期,我们只用关注业务逻辑即可
使用方法为将业务逻辑func传进去,例如:
db.Transaction(func(tx *gorm.DB) error { // 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db') if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil { // 返回任何错误都会回滚事务 return err } if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil { return err } // 返回 nil 提交事务 return nil
})
Transaction方法流程如下:
- db.Begin开启事务
- 执行fc,把事务tx传进去
- 如果fc执行成功,执行
tx.Commit
提交事务 - 如果fc执行出错或发生panic,调
tx.Rollback
回滚事务
func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error) {panicked := trueif committer, ok := db.Statement.ConnPool.(TxCommitter); ok && committer != nil {// ...} else {// 开启事务tx := db.Begin(opts...)if tx.Error != nil {return tx.Error}defer func() {// 如果发生panic,或者执行出错,回滚if panicked || err != nil {tx.Rollback()}}()if err = fc(tx); err == nil {panicked = falsereturn tx.Commit().Error}}panicked = falsereturn
}
预编译
只要用database/sql配合mysql驱动,执行sql时一定走了预编译。要么是客户端预编译,要么是mysql服务端预编译
在gorm层面,PrepareStmt的目的是提高性能,而不是执行sql时从非预编译变成预编译
默认gorm层面的PrepareStmt为false,这里假设使用服务端预编译(连接mysql的dsn中interpolateParams为false),执行sql模板 + 参数
类型的操作时有如下流程:
- 往mysql发送sql模板,获得
stmtId
- 往msqyl发送stmtId + 参数,得到执行结果
- 往mysql发送释放stmt命令
如果使用gorm层面的PrepareStmt,会对sql.Stmt进行缓存,如果当前连接预编译过该Stmt,就能直接用
接下来看看gorm层面怎么处理预编译的
初始化db时(gorm.Open),如果config.PrepareStmt为true,使用预编译模式:
func Open(dialector Dialector, opts ...Option) (db *DB, err error) {// ...// 预编译模式if config.PrepareStmt {preparedStmt := NewPreparedStmtDB(db.ConnPool)db.cacheStore.Store(preparedStmtDBKey, preparedStmt)db.ConnPool = preparedStmt}db.Statement = &Statement{DB: db,ConnPool: db.ConnPool,Context: context.Background(),Clauses: map[string]clause.Clause{},}// ...return
}
ConnPool被替换成PreparedStmtDB,其结构如下:
type PreparedStmtDB struct {// key: sql模板,value:StmtStmts map[string]*StmtMux *sync.RWMutex// 内置的 ConnPool 字段通常为 database/sql 中的 *DBConnPool
}
初始化PreparedStmtDB,内部持有sql.DB
func NewPreparedStmtDB(connPool ConnPool) *PreparedStmtDB {return &PreparedStmtDB{// 持有sql.DBConnPool: connPool,// sql到Stmt的映射Stmts: make(map[string]*Stmt),Mux: &sync.RWMutex{},}
}
可以看出PreparedStmtDB拥有sql模板到Stmt的缓存,那么遇到相同sql时,如果之前已经预编译过,就能用该Stmt执行db操作
后续基于这个db执行任何操作时,分为两个步骤:
- 通过
PreparedStmtDB.prepare(...)
操作创建/复用 stmt,后续相同 sql 模板可以复用此 stmt - 通过
stmt.Query(...)/Exec(...)
执行 sql
例如在执行PreparedStmtDB.QueryContext时:
- 先调PreparedStmtDB.prepare看有没有可复用的sql.Stmt
- 再调sql.Stmt执行QueryContext操作
func (db *PreparedStmtDB) QueryContext(ctx context.Context, query string, args ...interface{}) (rows *sql.Rows, err error) {// 先获取stmtstmt, err := db.prepare(ctx, db.ConnPool, false, query)if err == nil {// 再用stmt执行sqlrows, err = stmt.QueryContext(ctx, args...)if errors.Is(err, driver.ErrBadConn) {db.Mux.Lock()defer db.Mux.Unlock()go stmt.Close()delete(db.Stmts, query)}}return rows, err
}
PreparedStmtDB.prepare:
- 尝试从缓存Stmts中,根据query模板找sql.Stmt,如果有就返回
- 否则调sql.DB,根据query模板生成一个sql.Stmt,加入缓存中
func (db *PreparedStmtDB) prepare(ctx context.Context, conn ConnPool, isTransaction bool, query string) (Stmt, error) {db.Mux.RLock()// 以sql为模板,先查有没有可复用的stmt// 如果stmt.Transaction为false,可以复用if stmt, ok := db.Stmts[query]; ok && (!stmt.Transaction || isTransaction) {db.Mux.RUnlock()// wait for other goroutines prepared<-stmt.preparedif stmt.prepareErr != nil {return Stmt{}, stmt.prepareErr}return *stmt, nil}db.Mux.RUnlock()db.Mux.Lock()// double checkif stmt, ok := db.Stmts[query]; ok && (!stmt.Transaction || isTransaction) {db.Mux.Unlock()// wait for other goroutines prepared<-stmt.preparedif stmt.prepareErr != nil {return Stmt{}, stmt.prepareErr}return *stmt, nil}// ...// 到这里没有可复用的模板// 创建stmt实例,加到map中cacheStmt := Stmt{Transaction: isTransaction, prepared: make(chan struct{})}db.Stmts[query] = &cacheStmtdb.Mux.Unlock()// prepare completeddefer close(cacheStmt.prepared)// 调sql.DB的prepareContext方法,创建sql.stmtstmt, err := conn.PrepareContext(ctx, query)if err != nil {cacheStmt.prepareErr = errdb.Mux.Lock()delete(db.Stmts, query)db.Mux.Unlock()return Stmt{}, err}db.Mux.Lock()cacheStmt.Stmt = stmtdb.Mux.Unlock()return cacheStmt, nil
}
这里是ORM层面对Stmt做了缓存
而在go标准库database/sql层面,stmt要能执行的前提是,在当前连接预编过
我们看sql.Stmt源码,注释上有这么一段话:
A Stmt is safe for concurrent use by multiple goroutines
翻译:Stmt能被多个g并发调用
When the Stmt needs to execute on a new underlying connection, it will prepare itself on the new connection automatically
翻译:当Stmt需要被新的连接使用时,需要在新连接上预编译
sql.Stmt有一个css
slice,存放预编译了该Stmt的连接
type Stmt struct { // 持有driverStmtcgds *driverStmt // ...// 预编译了该Stmt的连接css []connStmt }type connStmt struct { dc *driverConn ds *driverStmt
}
基于Stmt执行Query, Exec
操作时,会检查当前使用的连接在不在Stmt.css
里面,如果在就能立即执行,否则需要先预编译该sql模板才能执行
这里以Stmt.ExecQuery为例,看看如何获取连接并执行
func (s *Stmt) ExecContext(ctx context.Context, args ...any) (Result, error) { s.closemu.RLock() defer s.closemu.RUnlock() var res Result err := s.db.retry(func(strategy connReuseStrategy) error {// 获取连接 dc, releaseConn, ds, err := s.connStmt(ctx, strategy) if err != nil { return err } // 执行execres, err = resultFromStatement(ctx, dc.ci, ds, args...) releaseConn(err) return err }) return res, err
}
重点在获取连接:
- 从连接池获取一个连接
- 检查该连接是否在Stmt.css里面,如果是,直接返回
- 否则需要先用该连接预编译sql模板
func (s *Stmt) connStmt(ctx context.Context, strategy connReuseStrategy) (dc *driverConn, releaseConn func(error), ds *driverStmt, err error) { // ...// 从连接池获取一个连接dc, err = s.db.conn(ctx, strategy) if err != nil { return nil, nil, nil, err } s.mu.Lock() // 检查该连接是否在Stmt.css里面,如果是,直接返回for _, v := range s.css { if v.dc == dc { s.mu.Unlock() return dc, dc.releaseConn, v.ds, nil } } s.mu.Unlock() // 否则需要先用该连接预编译sql模板withLock(dc, func() { ds, err = s.prepareOnConnLocked(ctx, dc) }) if err != nil { dc.releaseConn(err) return nil, nil, nil, err } return dc, dc.releaseConn, ds, nil
}
prepareOnConnLocked:预编译完成后,将连接加入stmt.css
func (s *Stmt) prepareOnConnLocked(ctx context.Context, dc *driverConn) (*driverStmt, error) { si, err := dc.prepareLocked(ctx, s.cg, s.query) if err != nil { return nil, err } cs := connStmt{dc, si} s.mu.Lock() // 预编译完成后,将当前连接加入stmt.csss.css = append(s.css, cs) s.mu.Unlock() return cs.ds, nil
}
事务结合预编译
如果同时有事务和预编译,那么在执行Exec/Query时,稍微有点不一样
回到事务的Begin方法:
func (db *DB) Begin(opts ...*sql.TxOptions) *DB { // ...switch beginner := tx.Statement.ConnPool.(type) { // ... // 预编译模式下: case ConnPoolBeginner: tx.Statement.ConnPool, err = beginner.BeginTx(tx.Statement.Context, opt) default: err = ErrInvalidTransaction } if err != nil { tx.AddError(err) } return tx
}
如果是预编译模式,进入PreparedStmtDB.BeginTx,返回PreparedStmtTX实例
func (db *PreparedStmtDB) BeginTx(ctx context.Context, opt *sql.TxOptions) (ConnPool, error) { if beginner, ok := db.ConnPool.(TxBeginner); ok { tx, err := beginner.BeginTx(ctx, opt) return &PreparedStmtTX{PreparedStmtDB: db, Tx: tx}, err } // ...
}
接下来看看基于PreparedStmtTX执行增删改查有何特别的地方
这里以PreparedStmtTX.ExecContext为例:
- 调
PreparedStmtDB.prepare
从缓存拿或新建一个Stmt - 调
sql.Tx.StmtContext
先处理步骤1返回的Stmt,再执行Exec,重点在这里
func (tx *PreparedStmtTX) ExecContext(ctx context.Context, query string, args ...interface{}) (result sql.Result, err error) { stmt, err := tx.PreparedStmtDB.prepare(ctx, tx.Tx, true, query) if err == nil { result, err = tx.Tx.StmtContext(ctx, stmt.Stmt).ExecContext(ctx, args...) if errors.Is(err, driver.ErrBadConn) { tx.PreparedStmtDB.Mux.Lock() defer tx.PreparedStmtDB.Mux.Unlock() go stmt.Close() delete(tx.PreparedStmtDB.Stmts, query) } } return result, err
}
sql.Tx.StmtContext方法
- 拿到和事务tx绑定的连接dc
- 看dc是否预编译过该stmt,如果没有执行预编译操作
- 将事务tx放到
Stmt.cg
字段,后面执行Exec获取连接时,优先从该字段获取- 也就是保证执行整个事务都要用同一个连接
func (tx *Tx) StmtContext(ctx context.Context, stmt *Stmt) *Stmt { dc, release, err := tx.grabConn(ctx) if err != nil { return &Stmt{stickyErr: err} } defer release(nil) if tx.db != stmt.db { return &Stmt{stickyErr: errors.New("sql: Tx.Stmt: statement from different database used")} } var si driver.Stmt var parentStmt *Stmt stmt.mu.Lock() if stmt.closed || stmt.cg != nil { // ...} else { stmt.removeClosedStmtLocked() // See if the statement has already been prepared on this connection, // and reuse it if possible. for _, v := range stmt.css { if v.dc == dc { si = v.ds.si break } } stmt.mu.Unlock() // 没预编译过,执行预编译if si == nil { var ds *driverStmt withLock(dc, func() { ds, err = stmt.prepareOnConnLocked(ctx, dc) }) if err != nil { return &Stmt{stickyErr: err} } si = ds.si } parentStmt = stmt } txs := &Stmt{ db: tx.db, // 重点在这,将tx放到Stmt.cg中。后面执行Exec获取连接时,优先从该字段获取cg: tx, cgds: &driverStmt{ Locker: dc, si: si, }, parentStmt: parentStmt, query: stmt.query, } if parentStmt != nil { tx.db.addDep(parentStmt, txs) } tx.stmts.Lock() tx.stmts.v = append(tx.stmts.v, txs) tx.stmts.Unlock() return txs
}
总结
至此,gorm的源码分析告一段落了,下一篇文章会介绍一些工程上使用gorm的最佳实践
相关文章:

gorm源码解析(四):事务,预编译
文章目录 前言事务自己控制事务用 Transaction方法包装事务 预编译事务结合预编译总结 前言 前几篇文章介绍gorm的整体设计,增删改查的具体实现流程。本文将聚焦与事务和预编译部分 事务 自己控制事务 用gorm框架,可以自己控制事务的Begin࿰…...

前端优雅(装逼)写法(updating····)
1.>>右位移运算符取整数 它将一个数字的二进制位向右移动指定的位数,并在左侧填充符号位(即负数用1填充,正数用0填充)。 比如 2.99934 >> 0:取整结果是2,此处取整并非四舍五入 2.99934 会先…...

黑马Java面试教程_P7_常见集合_P4_HashMap
系列博客目录 文章目录 系列博客目录4. HashMap相关面试题4.4 面试题-HashMap的put方法的具体流程 频54.4.1 hashMap常见属性4.4.2 源码分析 HashMap的构造函数面试文稿: 4.5 讲一讲HashMap的扩容机制 难3频4面试文稿: 4.6 面试题-hashMap的寻址算法 难4…...

使用 CFD 加强水资源管理:全面概述
探索 CFD(计算流体动力学)在增强保护人类健康的土木和水利工程实践方面的重大贡献。 挑战 水资源管理是指规划、开发、分配和管理水资源最佳利用的做法。它包括广泛的活动,旨在确保水得到有效和可持续的利用,以满足各种需求&…...

XXE练习
pikachu-XXE靶场 1.POC:攻击测试 <?xml version"1.0"?> <!DOCTYPE foo [ <!ENTITY xxe "a">]> <foo>&xxe;</foo> 2.EXP:查看文件 <?xml version"1.0"?> <!DOCTYPE foo [ <!ENTITY xxe SY…...

R语言读取hallmarks的gmt文档的不同姿势整理
不同格式各有所用 1.读取数据框格式的 hallmarks <- clusterProfiler::read.gmt("~/genelist/h.all.v7.4.symbols.gmt") #返回的是表格 hallmarks$term<- gsub(HALLMARK_,"",hallmarks$term)适配Y叔的clusterProfiler的后续分析,比如整理后geneli…...
【Nginx-4】Nginx负载均衡策略详解
在现代Web应用中,随着用户访问量的增加,单台服务器往往难以承受巨大的流量压力。为了解决这一问题,负载均衡技术应运而生。Nginx作为一款高性能的Web服务器和反向代理服务器,提供了多种负载均衡策略,能够有效地将请求分…...

Python 的 Decimal的错误计算
摘要 阐述在使用 Python的 Decimal类时,可能产生的错误计算。 在 详述 BigDecimal 的错误计算 中,笔者较为详细地说明了 Java的 BigDecimal可能出错的原因。类似地,Python的 decimal模块中有个 Decimal类,也可用于高精度的十进制…...

【韩顺平 Java满汉楼项目 控制台版】MySQL+JDBC+druid
文章目录 功能界面用户登录界面显示餐桌状态预定显示所有菜品点餐查看账单结账退出满汉楼 程序框架图项目依赖项目结构方法调用图功能实现登录显示餐桌状态订座显示所有菜品点餐查看账单结账退出满汉楼 扩展思考多表查询如果将来字段越来越多怎么办? 员工信息字段可…...

【HAL库】STM32CubeMX开发----STM32F407----Time定时器中断实验
STM32CubeMX 下载和安装 详细教程 【HAL库】STM32CubeMX开发----STM32F407----目录 前言 本次实验以 STM32F407VET6 芯片为MCU,使用 25MHz 外部时钟源。 实现定时器TIM3中断,每1s进一次中断。 定时器计算公式如下: arr 是自动装载值&#x…...

react18+ts 封装图表及词云组件
react18ts 封装图表及词云组件 1.下载依赖包 "echarts": "^5.5.1","echarts-for-react": "^3.0.2","echarts-wordcloud": "^2.1.0",2.创建目录结构 3.代码封装 ChartCard.tsx Wordcloud.tsx 4.调用 import Rea…...
图像根据mask拼接时,边缘有色差 解决
目录 渐变融合(Feathering) 沿着轮廓线模糊: 代码: 泊松融合 效果比较好: 效果图: 源代码: 泊松融合,mask不扩大试验 效果图: 源代码: 两个图像根据mask拼接时,边缘有色差 渐变融合(Feathering) import numpy as np import cv2# 假设 img1, img2 是两个…...

17、ConvMixer模型原理及其PyTorch逐行实现
文章目录 1. 重点2. 思维导图 1. 重点 patch embedding : 将图形分割成不重叠的块作为图片样本特征depth wise point wise new conv2d : 将传统的卷积转换成通道隔离卷积和像素空间隔离两个部分,在保证精度下降不多的情况下大大减少参数量 2. 思维导图 后续再整…...
Spring整合Redis基本操作步骤
Spring 整合 Redis 操作步骤总结 1. 添加依赖 首先,在 pom.xml 文件中添加必要的 Maven 依赖。Redis 相关的依赖包括 Spring Boot 的 Redis 启动器和 fastjson(如果需要使用 Fastjson 作为序列化工具): <!-- Spring Boot Re…...

STM32使用SFUD库驱动W25Q64
SFUD简介 SFUD是一个通用SPI Flash驱动库,通过SFUD可以库轻松完成对SPI Flash的读/擦/写的基本操作,而不用自己去看手册,写代码造轮子。但是SFUD的功能不仅仅于此:①通过SFUD库可以实现在一个项目中对多个Flash的同时驱动&#x…...

ArKTS基础组件
一.AlphabetIndexer 可以与容器组件联动用于按逻辑结构快速定位容器显示区域的组件。 子组件 color:设置文字颜色。 参数名类型必填说明valueResourceColor是 文字颜色。 默认值:0x99182431。 selectedColor:设置选中项文字颜色。 参数名类型必填说明valueRes…...
如何理解TCP/IP协议?如何理解TCP/IP协议是什么?
理解TCP/IP协议 1. 什么是TCP/IP协议? TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是一组用于实现网络通信的协议,广泛用于互联网和局域网中。TCP/IP协议栈由一系列协议组成,规定了计算机如何在网络中发送和接收数据。它通常被用来…...
如何使用 Python 连接 SQLite 数据库?
SQLite是一种轻量级的嵌入式数据库,广泛应用于各种应用程序中。 Python提供了内置的sqlite3模块,使得连接和操作SQLite数据库变得非常简单。 下面我将详细介绍如何使用sqlite3模块来连接SQLite数据库,并提供一些实际开发中的建议和注意事项…...

【博弈模型】古诺模型、stackelberg博弈模型、伯特兰德模型、价格领导模型
博弈模型 1、古诺模型(cournot)(1)假设(2)行为分析(3)经济后果(4)例题 2、stackelberg博弈模型(产量领导模型)(1ÿ…...
单片机:实现花样灯数码管的显示(附带源码)
单片机实现花样灯数码管显示 数码管(七段数码管)广泛用于数字显示,例如时钟、计数器、温度计等设备。在本项目中,我们将使用单片机实现花样灯数码管的显示效果。所谓花样灯显示是指通过控制数码管上的各个段位,以不同…...

《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》
引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...
Java 8 Stream API 入门到实践详解
一、告别 for 循环! 传统痛点: Java 8 之前,集合操作离不开冗长的 for 循环和匿名类。例如,过滤列表中的偶数: List<Integer> list Arrays.asList(1, 2, 3, 4, 5); List<Integer> evens new ArrayList…...
Admin.Net中的消息通信SignalR解释
定义集线器接口 IOnlineUserHub public interface IOnlineUserHub {/// 在线用户列表Task OnlineUserList(OnlineUserList context);/// 强制下线Task ForceOffline(object context);/// 发布站内消息Task PublicNotice(SysNotice context);/// 接收消息Task ReceiveMessage(…...
多场景 OkHttpClient 管理器 - Android 网络通信解决方案
下面是一个完整的 Android 实现,展示如何创建和管理多个 OkHttpClient 实例,分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...

Keil 中设置 STM32 Flash 和 RAM 地址详解
文章目录 Keil 中设置 STM32 Flash 和 RAM 地址详解一、Flash 和 RAM 配置界面(Target 选项卡)1. IROM1(用于配置 Flash)2. IRAM1(用于配置 RAM)二、链接器设置界面(Linker 选项卡)1. 勾选“Use Memory Layout from Target Dialog”2. 查看链接器参数(如果没有勾选上面…...
论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一)
宇树机器人多姿态起立控制强化学习框架论文解析 论文解读:交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一) 论文解读:交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化…...
工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配
AI3D视觉的工业赋能者 迁移科技成立于2017年,作为行业领先的3D工业相机及视觉系统供应商,累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成,通过稳定、易用、高回报的AI3D视觉系统,为汽车、新能源、金属制造等行…...

多种风格导航菜单 HTML 实现(附源码)
下面我将为您展示 6 种不同风格的导航菜单实现,每种都包含完整 HTML、CSS 和 JavaScript 代码。 1. 简约水平导航栏 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport&qu…...

HarmonyOS运动开发:如何用mpchart绘制运动配速图表
##鸿蒙核心技术##运动开发##Sensor Service Kit(传感器服务)# 前言 在运动类应用中,运动数据的可视化是提升用户体验的重要环节。通过直观的图表展示运动过程中的关键数据,如配速、距离、卡路里消耗等,用户可以更清晰…...

视频行为标注工具BehaviLabel(源码+使用介绍+Windows.Exe版本)
前言: 最近在做行为检测相关的模型,用的是时空图卷积网络(STGCN),但原有kinetic-400数据集数据质量较低,需要进行细粒度的标注,同时粗略搜了下已有开源工具基本都集中于图像分割这块,…...