Go第三方框架--gorm框架(一)

前言

orm模型简介

orm模型全称是Object-Relational Mapping,即对象关系映射。其实就是在原生sql基础之上进行更高程度的封装。方便程序员采用面向对象的方式来操作数据库,将表映射成对象。
这种映射带来几个好处:

  1. 代码简洁:不用手动编写sql
  2. 安全性提高了:orm框架会自动处理参数化查询,可以有效减少sql注入的风险
  3. 可读性提高了:orm更接近自然语言。
  4. 可移植性提高,切换不同数据库时,不用重新编写sql,只需要更改连接字符串。
gorm模型简介

gorm顾名思义是采用go语言实现的orm框架,由jinzhu开发后开源,又融合了很多大神的思路。gorm主要是在go原生数据库包database/sql 的基础上引入orm思想来编写。所以其底层仍会调用database/sql的相关增删改查操作等dml操作和表结构修改等ddl操作。
以原生database/sql 查询为例:

要实现如下sql(gorm可以操作多种据库,我们本篇只以mysql为例来介绍)

SELECT * FROM userinfos WHERE name = "lisan” ORDER BY id ASC LIMIT 1;

则 database/sql 代码如下

	dbsql, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/world?charset=utf8mb4&parseTime=True&loc=Local")
	if err != nil {
		log.Fatal(err)
	}
	defer dbsql.Close()
	query := "SELECT * FROM userinfos WHERE name = ? ORDER BY id ASC LIMIT 1"

	// 执行查询
	_, err = dbsql.Query(query, "lisan")
	if err != nil {
		log.Fatal(err)
	}

debug可以看到其调用的是database/sql的QueryContext(…)函数。
在这里插入图片描述
如果用gorm来实现相同功能sql,则代码如下:

	//gorm
	dsn := "root:root@tcp(127.0.0.1:3306)/world?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})  // db是初始化的db cloen=1 或 2 时,会将初始db复制一份,避免操作污染。
	if err != nil {
		panic(err)
	}
	var userinfo Userinfo
	// 查询单条db.Table(...) 中由于 db.clone=1 所以复制一份 新的db.clone=0 所以后续的链式调用 (各种. . .)的状态都会累加到 db.Table()生成的新db上,
	// 如此保证了 原始db的纯净 和 链式调用(拼装一条完整sql)的累加。(后续会有详细讲解)
	if err = db.Table("userinfos").Where("name = ?", "lisan").First(&userinfo).Error; err != nil {
 		return
	}

debug可以看到 其在First(…)函数内部也会调用 database/sql的QueryContext(…)函数
在这里插入图片描述
其调用链是

First(...)---->Execute(tx)----->f(db)----> Query(db *gorm.DB)---->db.Statement.ConnPool.QueryContext(....)---->QueryContext(....)

我们介绍gorm也是从具体的例子开始,到调用具体的database/sql函数为止。其实gorm可以看做将原生database/sql语句可配置化,自动对应表结构和sql语句进行封装。
至于database/sql调用的具体细节,后续会单独编写文章。好了明确了边界,现在我们来看下orm的几个重要结构体。

几种结构体

DB

db是gorm的核心结构体,所有操作都由它承载。

type DB struct {
	*Config     // 存放一些初始化的配制信息,包括连接池和执行dml的回调函数等
	Error        error // 执行操作的错误信息
	RowsAffected int64
	Statement    *Statement //  执行增删改查的 状态信息 ,是sql语句累加的地方。
	clone        int        // db克隆次数 用来克隆db实例 避免多个查询时 互相影响 保证每个操作都会复制一份初始化的db 
							// clone值 0:表示不复制实例 每个操作都会累计, 一般在相同sql语句中使用,例如 几个 where().where()链式调用需要进行sql状态累计。
							//   1:不同sql语句的增删改查等操作用,会复制一份新db,避免相互之间影响。一般只有连接池,配置等初始参数会复用,操作相关参数会初始化。相同语句链式调用会操作同一份Statement,不同的sql语句执行不同的Statement
							//   2: 开启事务时使用,配置直接复用,Statement会复制一份。 todo
}

Config结构体如下

Config

Config有初始化db需要的一些配置和初始化后的一些属性,比如连接池和回调函数等。

type Config struct {
	// GORM perform single create, update, delete operations in transactions by default to ensure database data  
	// ... 省略一些本篇不用的属性
	// ClauseBuilders clause builder
	ClauseBuilders map[string]clause.ClauseBuilder
	// ConnPool db conn pool  // 连接池 对应database/sql 中的sql.DB 存放连接状态等信息 防止频繁创建连接
	ConnPool ConnPool
	// Dialector database dialector
	Dialector //  mysql/sqlite等数据的操作接口
	// Plugins registered plugins
	Plugins map[string]Plugin

	callbacks  *callbacks // 增删改查等 gorm高度封装回调database/sql函数在这里注册
	cacheStore *sync.Map
}

其中 Dialector主要实现各种数据库的连接等操作,其结构如下:

type Dialector interface {
	Name() string
	Initialize(*DB) error      // 数据库连接池等的初始化 databae/sql.db的初始化 和 注册回调的原生database/sql dml 封装函数等
	Migrator(db *DB) Migrator  // 装载操作 也就是承接 ddl的一些功能
	DataTypeOf(*schema.Field) string
	DefaultValueOf(*schema.Field) clause.Expression
	BindVarTo(writer clause.Writer, stmt *Statement, v interface{})
	QuoteTo(clause.Writer, string)
	Explain(sql string, vars ...interface{}) string
}

其中 ConnPool 是Config中对接各数据库操作的地方,其函数内部直接调用了database/sql的相关函数,结构体如下:

type ConnPool interface {
	PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) 
	ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) // 执行
	QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)  // 查询
	QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row 
}

目前 gorm集合了 mysql、sqlite等数据库的实现。
Config 中 callbacks属性 其结构体如下:

type callbacks struct {
	processors map[string]*processor
}

type processor struct {
	db        *DB
	Clauses   []string
	fns       []func(*DB) // 回调函数 query 等
	callbacks []*callback
}
Statement

Statement是状态,也就是拼接完整sql和执行sql时需要的信息。

type Statement struct {
	*DB
	TableExpr            *clause.Expr
	Table                string         // 操作的表名
	Model                interface{}   //  结构体,跟表对应 用来承接结果
	Unscoped             bool
	Dest                 interface{}   //  结构体,跟表对应
	ReflectValue         reflect.Value
	Clauses              map[string]clause.Clause // gorm执行语句 存储map 例如 Where("id=?",1) 则key:Where,value :包含 id和1,用来拼接最终sql; 累计各个dml的操作
	BuildClauses         []string // 某dml操作可能需要的操作关键字, SELECT ,FROM,FOR等在sql中出现的先后顺序排列。 先出现的先用来组合SQL,这样保证sql语句的合法性
	Distinct             bool
	Selects              []string // selected columns
	Omits                []string // omit columns
	Joins                []join
	Preloads             map[string][]interface{}
	Settings             sync.Map
	ConnPool             ConnPool      // 连接池
	Schema               *schema.Schema //  要执行操作的表对象的一些信息;比如:这张表属性列表、主键信息、表名;结构体和表名对应表。
	Context              context.Context
	RaiseErrorOnNotFound bool
	SkipHooks            bool
	SQL                  strings.Builder // 拼接后的最终 sql语句 入参用占位符代替
	Vars                 []interface{}   // SQL 属性的入参
	CurDestIndex         int
	attrs                []interface{}
	assigns              []interface{}
	scopes               []func(*DB) *DB
}

现在只是大概梳理下其结构体,有疑惑很正常,接下来我们开始进入内部了解下其原理。共分为四大部分:

  1. 初始化:介绍gorm.db初始化的一些操作,包括初始化db.ConnPool(也就是database/sql db的初始化),注册回调的原生database/sql dml 封装函数等。
  2. 自动装载:自动装载主要介绍表的自动创建,属性的增加等ddl操作。
  3. 增删改查:这块主要讲解 gorm 如何将封装程序的可装配函数(例如where链式调用)转化为复杂的sql语句,然后通过回调函数实现原生 database/sql 的dml操作。
  4. 事务: todo

初始化

初始化主要是初始化一些必要的参数,我们重点关注初始化对应数据库的连接池和注册dml操作的回调函数
初始化代码是示例的前几行,如下:

dsn := "root:root@tcp(127.0.0.1:3306)/world?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})  // db是初始化的db cloen=1 时,会将初始db复制一份,避免操作污染。
	if err != nil {
		panic(err)
	}

其中mysql.Open(dsn)是按照mysql数据库的链接逻辑解析dsn,然后赋值给mysql的Dialector实现结构体,用来初始化mysql连接池用。
我们来看下gorm.Open(…)函数的实现:

func Open(dialector Dialector, opts ...Option) (db *DB, err error) {
	// ...
	// clone=1时  会复制一份 db的核心属性 包括 statement、config等
	db = &DB{Config: config, clone: 1}

	// 回调函数初始化 
	db.callbacks = initializeCallbacks(db)

	if config.ClauseBuilders == nil {
		config.ClauseBuilders = map[string]clause.ClauseBuilder{}
	}

	// 这块 调用database/sql 根据不同的Dialector初始化不同数据库的ConnPool(就是database/db 的sql.db参数)等参数;并将dml回调函数填入callbacks
	if config.Dialector != nil {
		err = config.Dialector.Initialize(db)

		if err != nil {
			if db, _ := db.DB(); db != nil {
				_ = db.Close()
			}
		}
	}
 	// ...
	// 初始化Statement 
	db.Statement = &Statement{
		DB:       db,
		ConnPool: db.ConnPool,
		Context:  context.Background(),
		Clauses:  map[string]clause.Clause{},
	}

 // ...
}

其中核心逻辑在config.Dialector.Initialize(db)中,我们来看下:

func (dialector Dialector) Initialize(db *gorm.DB) (err error) {
	// ...
	if dialector.Conn != nil {
		db.ConnPool = dialector.Conn
	} else {

		// 这边对接 database/sql 开始初始化连接池操作
		db.ConnPool, err = sql.Open(dialector.DriverName, dialector.DSN)
		if err != nil {
			return err
		}
	}

    // ...
	// register callbacks
	// 加载dml操作的关键字,用来定位sql关键字的先后顺序,以便生成合法的sql。
	callbackConfig := &callbacks.Config{
		CreateClauses: CreateClauses,
		QueryClauses:  QueryClauses,
		UpdateClauses: UpdateClauses,
		DeleteClauses: DeleteClauses,
	}
	
	// ...
	// dml的callbacks函数在这里执行
	callbacks.RegisterDefaultCallbacks(db, callbackConfig)

	for k, v := range dialector.ClauseBuilders() {
		db.ClauseBuilders[k] = v
	}
	return
}

我们看到这里主要完成了 mysql连接池的和dml回调函数的初始化。

其中 callbacks.RegisterDefaultCallbacks(…)函数如下:

func RegisterDefaultCallbacks(db *gorm.DB, config *Config) {
 	// ...
	// 注册执行的回调函数会在 First()中 最后一行的Execute()执行函数的 fns列表调用 
	// Create() 返回create对应的 *processor指针 对此指针的修改会反映在 db.config.callbacks属性上
	createCallback := db.Callback().Create()
	// *processor.callbacks ([]*callback) 在这边初始化
	createCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
	createCallback.Register("gorm:before_create", BeforeCreate)
	createCallback.Register("gorm:save_before_associations", SaveBeforeAssociations(true))
	createCallback.Register("gorm:create", Create(config))
	createCallback.Register("gorm:save_after_associations", SaveAfterAssociations(true))
	createCallback.Register("gorm:after_create", AfterCreate)
	createCallback.Match(enableTransaction).Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction)
	createCallback.Clauses = config.CreateClauses
	
	// 查询回调函数注册 也就是后续DML章节讲解的 查询函数的注册
	queryCallback := db.Callback().Query()
	queryCallback.Register("gorm:query", Query)
 	// ...
 	
 	// 删除
	deleteCallback := db.Callback().Delete()
	deleteCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
 	// ...
	
	// 更新
	updateCallback := db.Callback().Update()
	updateCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
	updateCallback.Register("gorm:setup_reflect_value", SetupUpdateReflectValue)
 	// ...

	// gorm可以对增删改查进行 封装操作,使得操作更简单。也支持原生sql的查询
	rowCallback := db.Callback().Row() 
	rowCallback.Register("gorm:row", RowQuery)
	rowCallback.Clauses = config.QueryClauses
	// ...
}

我们来看下涉及的结构体之间的关系图:
todo

到这里初始化我们需要关注的两个领域已将讲解完毕。

自动装载(ddl操作)

自动装载主要是用来实现ddl相关的此操作,比如表的创建,属性的增加,属性参数的修改,添加约束条件,添加索引等。其会动态感知结构体的字段变化,从而将其映射到表结构上。我们来看下其源码。先看下下面的例子:

type Userinfo struct {
	Id     uint
	Name   string
	Gender string
	Hobby  string
	Addr   string
	Age    uint8
}

func TestGorm(t *testing.T) {
	//gorm
	dsn := "root:root@tcp(127.0.0.1:3306)/world?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}

	//自动迁移 ddl相关的操作 调用 excute函数执行操作,调用"row"或者"raw"对应的 processor 执行查询 执行原生操作
	err = db.AutoMigrate(&Userinfo{})
	if err != nil {
		return
	}
}

执行如上语句后会创建 表名为 userinfos的表
在这里插入图片描述
其代码比较简单,但内部有复杂的逻辑,我们来简单梳理下。

自动装载源码如下:

// AutoMigrate run auto migration for given models
func (db *DB) AutoMigrate(dst ...interface{}) error {
	return db.Migrator().AutoMigrate(dst...)
}

其中 **Migrator()**方法返回值是Migrator 接口,其承载着自动装载需要的所有方法。todo

func (db *DB) Migrator() Migrator {
	tx := db.getInstance()  

	// apply scopes to migrator
	for len(tx.Statement.scopes) > 0 {
		tx = tx.executeScopes()
	}
	 // 调用 Dialector 的 Migrator 方法,传入一个 Session 实例。Session 包含了当前的事务信息,用于执行迁移操作 
	return tx.Dialector.Migrator(tx.Session(&Session{}))
}

Migrator 接口如下:

// ddl相关操作 可以增删 表 表的属性 视图 限制条件 索引 等;可以查看数据库
type Migrator interface {
	// AutoMigrate
	AutoMigrate(dst ...interface{}) error 

	// Database 数据库相关操作
	CurrentDatabase() string
	FullDataTypeOf(*schema.Field) clause.Expr
	GetTypeAliases(databaseTypeName string) []string

	// Tables 表相关操作
	CreateTable(dst ...interface{}) error
	DropTable(dst ...interface{}) error
	// ...

	// Columns 列相关操作
	AddColumn(dst interface{}, field string) error
	DropColumn(dst interface{}, field string) error
 	// ...

	// Views 视图相关操作
	CreateView(name string, option ViewOption) error
	DropView(name string) error

	// Constraints 限制条件相关操作
	CreateConstraint(dst interface{}, name string) error
	DropConstraint(dst interface{}, name string) error
	HasConstraint(dst interface{}, name string) bool

	// Indexes 索引相关操作
	CreateIndex(dst interface{}, name string) error
	DropIndex(dst interface{}, name string) error
 	// ...
}

AutoMigrate(value …interface{})承载着,自动装载的核心逻辑,包括对表、属性、索引等的操作;源码如下:

// AutoMigrate auto migrate values
func (m Migrator) AutoMigrate(values ...interface{}) error {
	for _, value := range m.ReorderModels(values, true) {
		queryTx, execTx := m.GetQueryAndExecTx()

		// 没有找到对应表 需要创建表
		if !queryTx.Migrator().HasTable(value) {
			// 创建表
			if err := execTx.Migrator().CreateTable(value); err != nil {
				return err
			}
		} else {
			// 将结构体名映射成表名 然后执行回调函数
			if err := m.RunWithValue(value, func(stmt *gorm.Statement) error {
				columnTypes, err := queryTx.Migrator().ColumnTypes(value)
				if err != nil {
					return err
				}
				var (
					parseIndexes          = stmt.Schema.ParseIndexes()
					parseCheckConstraints = stmt.Schema.ParseCheckConstraints()
				)

				// DBNames 结构体 属性 按照 规则转换成 表属性 格式
				for _, dbName := range stmt.Schema.DBNames {
					var foundColumn gorm.ColumnType
					// columnTypes: 从数据库获得 表的 属性名信息 columnTypes
					for _, columnType := range columnTypes {
						if columnType.Name() == dbName {
							foundColumn = columnType
							break
						}
					}

					// 表中没有对应列
					if foundColumn == nil {
						// not found, add column 创建列
						if err = execTx.Migrator().AddColumn(value, dbName); err != nil {
							return err
						}
					} else {
						// found, smartly migrate 找到了结构体属性对应列名  对属性的参数(类型,注释等)进行 更新
						field := stmt.Schema.FieldsByDBName[dbName]
						if err = execTx.Migrator().MigrateColumn(value, field, foundColumn); err != nil {
							return err
						}
					}
				}

				// 对表约束条件进行更新
				if !m.DB.DisableForeignKeyConstraintWhenMigrating && !m.DB.IgnoreRelationshipsWhenMigrating {
					// ...
				}

				// 对表索引进行更新
				for _, idx := range parseIndexes {
					if !queryTx.Migrator().HasIndex(value, idx.Name) {
						if err := execTx.Migrator().CreateIndex(value, idx.Name); err != nil {
							return err
						}
					}
				}

				return nil
			}); err != nil {
				return err
			}
		}
	}

	return nil
}

AutoMigrate函数中需要的核心调用函数 都来自 Migrator 结构体 ,我们选择HasTable()函数来简单梳理下。
**HasTable(…)**用来判断是否存在特定表。其源码如下:

func (m Migrator) HasTable(value interface{}) bool {
	var count int64

	m.RunWithValue(value, func(stmt *gorm.Statement) error {
		currentDatabase := m.DB.Migrator().CurrentDatabase()
		// 原生sql执行(所谓执行原生sql,就是直接写原生sql,来调用database/sql方法,不使用gorm来组装sql),执行逻辑在Row()中,这边会调用已经注册的回调函数
		return m.DB.Raw("SELECT count(*) FROM information_schema.tables WHERE table_schema = ? AND table_name = ? AND table_type = ?", currentDatabase, stmt.Table, "BASE TABLE").Row().Scan(&count)
	})

	return count > 0
}

自动装载中所有对sql的调用的都是原生sql,因为查表、添加属性这种ddl sql语句比较固定,所以没必要采用组装的形式;而增删改查等 复杂的dml可以采用gorm来组装(比如:where(…).where(…)这种gorm最终会组合成sql语句。对database/sql的调用可以看做是接口调用,自动装载中的dml操作就简单的处理下得到了入参),再啰嗦一句,增删改等复杂的dml操作为用户提供了链式组合的方式来编写复杂的sql,它将组合的sql语句链,经过一些列的操作转换成调用原生sql的入参(原生sql和sql语句入参)。
自动装载函数AutoMigrate中还有好多值得深挖的点,由于篇幅原因不做介绍,感兴趣的大神可以深挖下。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/900161.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AVL树介绍与构建

目录 AVL树的概念 二叉树的构建 平衡因子的更新 旋转 左单旋 旋转过程 左单旋代码 右单旋 旋转过程 右单旋代码 左右双旋 发生情况 抽象图 具体图 平衡因子更新 左右双旋代码 右左双旋 右左双旋旋代码 验证测试AVL树 测试成员函数 测试代码 AVL树实现代码…

使用virtualenv导入ssl模块找不到指定的模块

最近在学习tensorflow&#xff0c;由于教程里面使用的是virtualenv&#xff0c;所以就按照教程开始安装了虚拟环境。但是在使用的时候&#xff0c;卡在了import ssl这一步&#xff0c;提示如下错误 >>> import ssl Traceback (most recent call last):File "<…

兼容Lodash的真正替代者

大家好&#xff0c;我是农村程序员&#xff0c;独立开发者&#xff0c;前端之虎陈随易。 这是我的个人网站&#xff1a;https://chensuiyi.me&#xff0c;欢迎一起交朋友~ 今天给大家分享一个前端工具库 Lodash 的替代品 es-toolkit。 仓库地址&#xff1a;https://github.com…

Android 自定义 Dialog 实现列表 单选,多选,搜索

前言 在Android开发中&#xff0c;通过对话框让用户选择&#xff0c;筛选信息是很方便也很常见的操作。本文详细介绍了如何使用自定义 Dialog、RecyclerView 以及自定义搜索框 来实现选中状态和用户交互&#xff0c;文中大本分代码都有明确注释&#xff0c;主打一个简单明了&a…

A survey of loss functions for semantic segmentation——论文笔记

摘要 图像分割一直是一个活跃的研究领域&#xff0c;因为它有着广泛的应用范围&#xff0c;从自动疾病检测到自动驾驶汽车。过去五年中&#xff0c;各种论文提出了用于不同场景&#xff08;如数据偏斜、稀疏分割等&#xff09;的目标损失函数。在本文中&#xff0c;我们总结了…

NVR监测软件/设备EasyNVR多个NVR同时管理构建智慧城市的大数据解决方案

在当今的数字化时代&#xff0c;安防视频监控已成为各行各业不可或缺的一部分&#xff0c;无论是在智慧工地、智慧工厂、智慧景区还是智慧水利等领域&#xff0c;都需要高效、可靠的监控系统来保障安全。 一、背景需求分析 随着科技的发展&#xff0c;智慧城市建设成为城市发展…

★ 算法OJ题 ★ 前缀和算法(上)

Ciallo&#xff5e;(∠・ω< )⌒☆ ~ 今天&#xff0c;将和大家一起做几道前缀和算法题 ~ ❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️ 澄岚主页&#xff1a;椎名澄嵐-CSDN博客 算法专栏&#xff1a;★ 优选算法100天 ★_椎名澄嵐的博客-CSDN博客 ❄️❄️❄…

毕业设计选题:基于Django+Vue的物资配送管理系统的设计与实现

开发语言&#xff1a;Python框架&#xff1a;djangoPython版本&#xff1a;python3.7.7数据库&#xff1a;mysql 5.7数据库工具&#xff1a;Navicat11开发软件&#xff1a;PyCharm 系统展示 管理员登录界面 管理员功能界面 申领者管理 后勤处管理 物资信息管理 入库信息管理 …

i春秋web题库——题目名称:SQLi

WEB——SQLi 写在之前&#xff1a;题目简介&#xff1a;题目分析&#xff1a; 写在之前&#xff1a; 本题在CSDN上或是其它博客上有过解答&#xff0c;只不过不知是什么原因&#xff0c;我没有找到解题过程比较完整的文章。于是我决定在CTF初学阶段写一篇这样的博客&#xff0…

ctfshow(78->81)--文件包含漏洞

Web78 源代码如下&#xff1a; if(isset($_GET[file])){$file $_GET[file];include($file); }else{highlight_file(__FILE__);代码审计&#xff1a; 使用 include() 进行文件包含&#xff0c;通过GET方法 传递参数file 获取被包含的文件。 思路&#xff1a; 利用 data://…

已解决:Uncaught SyntaxError: Unexpected token <

已解决&#xff1a;Uncaught SyntaxError: Unexpected token < 文章目录 写在前面问题描述报错原因分析 解决思路解决办法1. 使用开发者工具检查网络请求2. 验证脚本路径3. 检查服务器配置4. 处理跨域请求5. 检查打包工具配置6. 确认内容类型7. 使用原始文件进行测试8. 处理…

基于SpringBoot的电商网站源码(前台+后台)

主要功能流程&#xff1a; 前台购物&#xff1a;用户在前台进行商品浏览、选购及下单。个人中心查看&#xff1a;用户登录后可在个人中心查看订单状态、个人信息等。后台发货&#xff1a;管理员在后台系统处理订单&#xff0c;包括发货、退款等操作。 运行环境&#xff1a; …

iOS 18.2 可让欧盟用户删除App Store、Safari、信息、相机和照片应用

升级到 iOS 18.2 之后&#xff0c;欧盟的 iPhone 用户可以完全删除一些核心应用程序&#xff0c;包括 App Store、Safari、信息、相机和 Photos 。苹果在 8 月份表示&#xff0c;计划对其在欧盟的数字市场法案合规性进行更多修改&#xff0c;其中一项更新包括欧盟用户删除系统应…

张三进阶之路 | 基于Spring AOP的Log收集

前情提要 &#x1f4cc; 张三对于公司的日志处理系统不满意&#xff0c;认为其性能不佳且功能有限。为了展示自己的能力和技术实力&#xff0c;他决定利用Spring AOP&#xff08;面向切面编程&#xff09;开发一个更高效的日志处理系统&#xff0c;并将其存储在Redis中。 首先…

初识算法 · 二分查找(4)

目录 前言&#xff1a; 寻找峰值 题目解析 算法原理 算法编写 寻找旋转排序数组中的最小值 题目解析 算法原理 算法编写 寻找缺失的数字 题目解析 算法原理 算法编写 前言&#xff1a; ​本文的主题是二分查找&#xff0c;通过三道题目讲解&#xff0c;一道是寻找…

超越OpenAI GPT-4o,Yi-Lightning指南:中国AI大模型新巅峰

Yi-Lightning 是零一万物公司最新发布的旗舰模型&#xff0c;它在国际权威盲测榜单 LMSYS 上超越了硅谷知名 OpenAI GPT-4o-2024-05-13、Anthropic Claude 3.5 Sonnet&#xff0c;排名世界第六&#xff0c;中国第一&#xff0c;这标志着中国大模型首次实现超越 OpenAI GPT-4o 的…

InnoDB 存储引擎的底层逻辑架构白话-必知必会

目录标题 前言白话内存架构1. 自适应哈希索引自适应哈希索引的作用自适应哈希索引的工作原理自适应哈希索引与缓存的区别启用和禁用自适应哈希索引 2. Buffer pool3. Change buffer 下面我们就来详细分析一下&#xff0c;数据修改操作的步骤。4. Log Buffer 磁盘架构1. 系统表空…

一文了解AOSP是什么?

一文了解AOSP是什么&#xff1f; AOSP基本信息 基本定义 AOSP是Android Open Source Project的缩写&#xff0c;这是一个由Google维护的完全免费和开放的操作系统开发项目。它是Android系统的核心基础&#xff0c;提供了构建移动操作系统所需的基本组件。 主要特点 完全开源…

echarts散点图

一、类似散点图折线图不展示折线 option {grid: {left: 10,right: 20,top: 35,bottom: 15,containLabel: true},tooltip: {show: true,trigger: item,backgroundColor: "rgba(0,0,0,0)", // 提示框浮层的背景颜色。formatter: function (params) {var html <d…

基于Python的B站视频数据分析与可视化

基于Python的B站视频数据分析与可视化 爬取视频、UP主信息、视频评论 功能列表 关键词搜索指定帖子ID爬取指定UP主的主页爬取支持评论爬取生成评论词云图支持数据存在数据库支持可视化 部分效果演示 关键词搜索爬取 指定UP主的主页爬取 指定为黑马的了 爬取视频的时爬取评论…