Golang实践录:使用sqlx操作sqlite3数据库

本文结合sqlx给出一些操作数据库的示例,并将常见功能封装了函数接口形式。

问题提出

几年前,笔者写了个golang工程,是和数据库打交道的,当时曾经调查过xorm,但并不合适,于是使用较原始的方式实现功能,后来一直沿用。原始方式有一个好处,可以自由组合sql,达到不同的查询目的。

最近又要和数据库打交道,考虑到工程实用性,经研究,决定使用sqlx将常见功能做成内部接口形式。

实践

设计说明

本文使用前面文章生成的结构体作为演示数据表字段。为简单起见,id不设置自增。另外,使用反射手段进行建表、写表、查表等操作。

结构体定义:

复制代码
type ResultData_t struct {
	Id        string `json:"id" db:"id"`
	Num       int    `json:"num" db:"num"`
	Etype     int    `json:"etype" db:"etype"`
	Timestamp string `json:"timestamp" db:"timestamp"`
	MCode     string `json:"mCode" db:"mCode"`
	MName     string `json:"mName" db:"mName"`
	Age       string `json:"age" db:"age"`
	Addr      string `json:"addr" db:"addr"`
	Mac       string `json:"mac" db:"mac"`
	Remark    string `json:"remark" db:"remark"`
}

变量定义:

复制代码
var gTableName = "result_data"
var dbstr = "file:./database/result_data.db3?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=on"

引入第三方库

库如下:

复制代码
"github.com/jmoiron/sqlx"
_ "modernc.org/sqlite"

要说明的是,本文采用"modernc.org/sqlite",这个库实现了sqlite3,但不使用cgo,因此十分合适跨平台,注意,该库使用的数据库驱动名为sqlite,而不是sqlite3

笔者近年使用go作为主力语言,一个工程,如果不涉及底层库,都会优先考虑跨平台性。比如,同一套代码,在windows开发调试,再交叉编译成linux或arm版本,基本不用开虚拟机了。

初始化数据库

代码如下,代码会自动创建database/result_data.db3文件,默认表为result_data

复制代码
type ResultWriter struct {
	db *sqlx.DB
}

func NewDB3Writer() *ResultWriter {
	return &ResultWriter{}
}

func getDB3File(dbname string) string {
	if !strings.HasPrefix(dbname, "file:") {
		return ""
	}
	filePart := dbname[5:] // 移除 "file:"
	parts := strings.Split(filePart, "?")
	if len(parts) == 0 {
		return ""
	}

	return com.GetFileDir(parts[0])
}

func (w *ResultWriter) CreateResultWriter(dbname string) (err error) {
	// SQLite 连接字符串(包含并发优化参数)
	// "file:./database/result_data.db3?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=on"

	// 确保目录存在
	filePath := getDB3File(dbname)
	com.MkDir(filePath)

	dsn := dbname
	w.db, err = sqlx.Connect("sqlite", dsn)
	if err != nil {
		return err
	}

	w.db.SetMaxOpenConns(1) // SQLite 推荐单个连接
	w.db.SetMaxIdleConns(1)
	w.db.SetConnMaxLifetime(time.Hour)

	err = w.initTables()
	if err != nil {
		klog.Println("创建内部数据库失败: ", err.Error())
	}

	return nil
}

func (w *ResultWriter) initTables() error {
	// w.initTableSimple()
	// 使用反射创建表
	err := dbutil.CreateTableFromStruct(w.db, gTableName, ResultData_t{}, "id")
	if err != nil {
		return fmt.Errorf("创建 result_data 表失败: %v", err)
	}

	return nil
}

建表

在初始化的同时创建了数据表。可以调用initTableSimple,也可以调用CreateTableFromStruct。其中initTableSimple为直接指定字段创建表,实现函数如下:

复制代码
// 直接指定字段创建表
func (w *ResultWriter) initTableSimple() error {
	createTableSQL := `
		CREATE TABLE IF NOT EXISTS result_data (
			id TEXT PRIMARY KEY,
			num INTEGER NOT NULL,
			etype INTEGER NOT NULL,
			timestamp TEXT NOT NULL,
			mCode TEXT NOT NULL,
			mName TEXT NOT NULL,
			age TEXT NOT NULL,
			addr TEXT NOT NULL,
			mac TEXT NOT NULL,
			remark TEXT
		)
	`

	_, err := w.db.Exec(createTableSQL)
	if err != nil {
		return err
	}

	// 创建常用索引
	indexes := []string{
		"CREATE INDEX IF NOT EXISTS idx_result_data_timestamp ON result_data(timestamp)",
	}

	for _, indexSQL := range indexes {
		_, err = w.db.Exec(indexSQL)
		if err != nil {
			return fmt.Errorf("创建索引失败: %v", err)
		}
	}

	return nil
}

CreateTableFromStruct则使用结构体建表,通用性强一些,实现如下:

复制代码
func CreateTableFromStruct(db *sqlx.DB, tableName string, model interface{}, primaryKey string, indices ...string) error {
	if db == nil {
		return fmt.Errorf("数据库连接为空")
	}

	var t reflect.Type
	if model == nil {
		return fmt.Errorf("model 参数不能为 nil")
	}

	v := reflect.ValueOf(model)
	if v.Kind() == reflect.Ptr {
		v = v.Elem()
		t = v.Type()
	} else {
		t = reflect.TypeOf(model)
	}

	if t.Kind() != reflect.Struct {
		return fmt.Errorf("model 必须是结构体")
	}

	var columns []string
	var hasPrimaryKey bool

	// 遍历结构体字段
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)

		// 获取字段名(优先使用 db tag,其次使用字段名)
		columnName := field.Tag.Get("db")
		if columnName == "" || columnName == "-" {
			// 如果 db tag 为空或为 "-",跳过该字段
			continue
		}

		// 获取字段类型
		fieldType := field.Type

		// 处理指针类型
		if fieldType.Kind() == reflect.Ptr {
			fieldType = fieldType.Elem()
		}

		// 根据字段类型确定 SQL 类型
		var sqlType string
		switch fieldType.Kind() {
		case reflect.String:
			sqlType = "TEXT"
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			sqlType = "INTEGER"
		case reflect.Float32, reflect.Float64:
			sqlType = "REAL"
		case reflect.Bool:
			sqlType = "INTEGER" // SQLite 用 0/1 表示布尔值
		case reflect.Struct:
			// 处理 time.Time 类型
			if fieldType.String() == "time.Time" {
				sqlType = "TEXT"
			} else {
				sqlType = "TEXT" // 其他结构体序列化为 JSON 存储
			}
		case reflect.Slice, reflect.Array, reflect.Map:
			// 数组/切片/映射序列化为 JSON 存储
			sqlType = "TEXT"
		default:
			// 默认使用 TEXT 类型
			sqlType = "TEXT"
		}

		// 构建列定义
		columnDef := fmt.Sprintf("%s %s", columnName, sqlType)

		// 检查是否是主键
		// isPrimaryKey := false
		if primaryKey != "" {
			// 使用指定的主键
			if columnName == primaryKey {
				columnDef += " PRIMARY KEY"
				// isPrimaryKey = true
				hasPrimaryKey = true
			}
		} else {
			// 自动判断:如果字段名为 "id" 且没有显式指定主键
			if columnName == "id" && !hasPrimaryKey {
				columnDef += " PRIMARY KEY AUTOINCREMENT"
				// isPrimaryKey = true
				hasPrimaryKey = true
			}
		}

		// 检查 NOT NULL 约束
		notNullTag := field.Tag.Get("dbNotNull")
		if notNullTag == "true" || notNullTag == "1" {
			columnDef += " NOT NULL"
		}

		// 检查 UNIQUE 约束
		uniqueTag := field.Tag.Get("dbUnique")
		if uniqueTag == "true" || uniqueTag == "1" {
			columnDef += " UNIQUE"
		}

		// 添加 DEFAULT 值
		defaultTag := field.Tag.Get("dbDefault")
		if defaultTag != "" {
			columnDef += " DEFAULT " + defaultTag
		}

		columns = append(columns, columnDef)
	}

	if len(columns) == 0 {
		return fmt.Errorf("没有找到有效的字段")
	}

	// 建表语句
	createTableSQL := fmt.Sprintf(`
		CREATE TABLE IF NOT EXISTS %s (
			%s
		)
	`, tableName, strings.Join(columns, ",\n\t\t"))

	// 执行
	_, err := db.Exec(createTableSQL)
	if err != nil {
		return fmt.Errorf("创建表 %s 失败: %v", tableName, err)
	}

	// fmt.Printf("debug 创建表语句:[%v]\n", createTableSQL)

	// 创建索引
	for _, field := range indices {
		if field != "" {
			indexSQL := fmt.Sprintf("CREATE INDEX IF NOT EXISTS idx_%s_%s ON %s(%s)",
				tableName, field, tableName, field)
			_, err = db.Exec(indexSQL)
			if err != nil {
				return fmt.Errorf("创建索引 %s 失败: %v", field, err)
			}
		}
	}

	return nil
}

写表

复制代码
func InsertTable[T any](db *sqlx.DB, tableName string, model []T, skipid ...string) (int64, string, error) {
	if len(model) == 0 {
		return 0, "", nil
	}

	// 获取第一个记录的结构信息
	t := reflect.TypeOf(model[0])
	if t.Kind() == reflect.Ptr {
		t = t.Elem()
	}

	// 可指定过滤的字段名,有的库可能会使用自增的id,因此要过滤
	getskip := func(id string) bool {
		for i := range skipid {
			if skipid[i] == id {
				return true
			}
		}
		return false
	}

	// 收集所有 db tag(排除 id 字段)
	var fieldNames []string
	var dbTags []string

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		dbTag := field.Tag.Get("db")

		if dbTag != "" && dbTag != "-" && !getskip(dbTag) {
			fieldNames = append(fieldNames, field.Name)
			dbTags = append(dbTags, dbTag)
		}
	}

	// 构建带命名参数的 SQL
	var namedParams []string
	for _, tag := range dbTags {
		namedParams = append(namedParams, ":"+tag)
	}

	query := fmt.Sprintf(`
        INSERT INTO %s (%s)
        VALUES (%s)
    `, tableName,
		strings.Join(dbTags, ", "),
		strings.Join(namedParams, ", "))

	// 执行批量插入
	result, err := db.NamedExec(query, model)
	if err != nil {
		return 0, query, fmt.Errorf("表 %s: %v", tableName, err)
	}

	// fmt.Printf("debug insert: [%v]\n", query)

	rows, err := result.RowsAffected()
	if err != nil {
		return int64(len(model)), query, nil // 有些数据库可能不支持
	}

	return rows, query, nil
}

查表

查表同样使用了反射的技术,主要调用genSelectItem将结构体各字段组成select语句。

比如:

复制代码
SELECT id, num, etype, timestamp, mCode, mName, age, addr, mac, remark FROM result_data  WHERE id = 'id_8'

SELECT id, num, etype, timestamp, mCode, mName, age, addr, mac, remark FROM result_data

事实上,在结构体ResultData_t定义时,就使用了db指定了各字段的名称,如下:

复制代码
Id        string `json:"id" db:"id"`
Num       int    `json:"num" db:"num"`

这也是genSelectItem可处理任意结构体的原因,该函数见下文的实现。

查表分2类,一是单个查询QueryOne,调用者知道只有一个结果时,可使用该函数。如果查询多个,则使用QueryTable函数。

这2个函数实现如下:

复制代码
func QueryOne[T any](db *sqlx.DB, tableName string, model T, where string, args ...interface{}) (*T, error) {
	sqlstr := genSelectItem(model, tableName, where)

	var result T
	err := db.Get(&result, sqlstr, args...)
	if err != nil {
		return nil, fmt.Errorf("表 %s: %v", tableName, err)
	}

	return &result, nil
}

func QueryTable[T any](db *sqlx.DB, tableName string, model T, where string) ([]T, string, error) {
	mysqlstr := fmt.Sprintf("SELECT * FROM %v %v", tableName, where)

	sqlstr := genSelectItem(model, tableName, where)

	var results []T
	err := db.Select(&results, sqlstr)
	if err != nil {
		return nil, mysqlstr, fmt.Errorf("表 %s: %v", tableName, err)
	}

	return results, mysqlstr, nil
}

接口工具函数集

本文所用的工具函数集如下:

复制代码
package db

import (
	"fmt"
	"reflect"
	"strings"

	"github.com/jmoiron/sqlx"
)

func CreateTableFromStruct(db *sqlx.DB, tableName string, model interface{}, primaryKey string, indices ...string) error {
	if db == nil {
		return fmt.Errorf("数据库连接为空")
	}

	var t reflect.Type
	if model == nil {
		return fmt.Errorf("model 参数不能为 nil")
	}

	v := reflect.ValueOf(model)
	if v.Kind() == reflect.Ptr {
		v = v.Elem()
		t = v.Type()
	} else {
		t = reflect.TypeOf(model)
	}

	if t.Kind() != reflect.Struct {
		return fmt.Errorf("model 必须是结构体")
	}

	var columns []string
	var hasPrimaryKey bool

	// 遍历结构体字段
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)

		// 获取字段名(优先使用 db tag,其次使用字段名)
		columnName := field.Tag.Get("db")
		if columnName == "" || columnName == "-" {
			// 如果 db tag 为空或为 "-",跳过该字段
			continue
		}

		// 获取字段类型
		fieldType := field.Type

		// 处理指针类型
		if fieldType.Kind() == reflect.Ptr {
			fieldType = fieldType.Elem()
		}

		// 根据字段类型确定 SQL 类型
		var sqlType string
		switch fieldType.Kind() {
		case reflect.String:
			sqlType = "TEXT"
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			sqlType = "INTEGER"
		case reflect.Float32, reflect.Float64:
			sqlType = "REAL"
		case reflect.Bool:
			sqlType = "INTEGER" // SQLite 用 0/1 表示布尔值
		case reflect.Struct:
			// 处理 time.Time 类型
			if fieldType.String() == "time.Time" {
				sqlType = "TEXT"
			} else {
				sqlType = "TEXT" // 其他结构体序列化为 JSON 存储
			}
		case reflect.Slice, reflect.Array, reflect.Map:
			// 数组/切片/映射序列化为 JSON 存储
			sqlType = "TEXT"
		default:
			// 默认使用 TEXT 类型
			sqlType = "TEXT"
		}

		// 构建列定义
		columnDef := fmt.Sprintf("%s %s", columnName, sqlType)

		// 检查是否是主键
		// isPrimaryKey := false
		if primaryKey != "" {
			// 使用指定的主键
			if columnName == primaryKey {
				columnDef += " PRIMARY KEY"
				// isPrimaryKey = true
				hasPrimaryKey = true
			}
		} else {
			// 自动判断:如果字段名为 "id" 且没有显式指定主键
			if columnName == "id" && !hasPrimaryKey {
				columnDef += " PRIMARY KEY AUTOINCREMENT"
				// isPrimaryKey = true
				hasPrimaryKey = true
			}
		}

		// 检查 NOT NULL 约束
		notNullTag := field.Tag.Get("dbNotNull")
		if notNullTag == "true" || notNullTag == "1" {
			columnDef += " NOT NULL"
		}

		// 检查 UNIQUE 约束
		uniqueTag := field.Tag.Get("dbUnique")
		if uniqueTag == "true" || uniqueTag == "1" {
			columnDef += " UNIQUE"
		}

		// 添加 DEFAULT 值
		defaultTag := field.Tag.Get("dbDefault")
		if defaultTag != "" {
			columnDef += " DEFAULT " + defaultTag
		}

		columns = append(columns, columnDef)
	}

	if len(columns) == 0 {
		return fmt.Errorf("没有找到有效的字段")
	}

	// 建表语句
	createTableSQL := fmt.Sprintf(`
		CREATE TABLE IF NOT EXISTS %s (
			%s
		)
	`, tableName, strings.Join(columns, ",\n\t\t"))

	// 执行
	_, err := db.Exec(createTableSQL)
	if err != nil {
		return fmt.Errorf("创建表 %s 失败: %v", tableName, err)
	}

	// fmt.Printf("debug 创建表语句:[%v]\n", createTableSQL)

	// 创建索引
	for _, field := range indices {
		if field != "" {
			indexSQL := fmt.Sprintf("CREATE INDEX IF NOT EXISTS idx_%s_%s ON %s(%s)",
				tableName, field, tableName, field)
			_, err = db.Exec(indexSQL)
			if err != nil {
				return fmt.Errorf("创建索引 %s 失败: %v", field, err)
			}
		}
	}

	return nil
}

// 自动从结构体生成SELECT字段列表
func genSelectItem(model interface{}, tableName string, where1 ...string) string {
	t := reflect.TypeOf(model)
	if t.Kind() == reflect.Ptr {
		t = t.Elem()
	}

	var fields []string
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		dbTag := field.Tag.Get("db")
		if dbTag != "" && dbTag != "-" {
			fields = append(fields, dbTag)
		}
	}

	where := ""
	if len(where1) > 0 {
		where = where1[0]
	}
	return fmt.Sprintf("SELECT %s FROM %s %s", strings.Join(fields, ", "), tableName, where)
}

///////////////////////////

func QueryOne[T any](db *sqlx.DB, tableName string, model T, where string, args ...interface{}) (*T, error) {
	sqlstr := genSelectItem(model, tableName, where)

	var result T
	err := db.Get(&result, sqlstr, args...)
	if err != nil {
		// if err == sql.ErrNoRows {
		// 	return nil, err // 没有找到记录,返回 nil
		// }
		return nil, fmt.Errorf("表 %s: %v", tableName, err)
	}

	return &result, nil
}

func QueryTable[T any](db *sqlx.DB, tableName string, model T, where string) ([]T, string, error) {
	// start := time.Now()
	// defer func() {
	// 	stats := db.Stats()
	// 	fmt.Printf("查询耗时: %v, 等待连接数量: %d\n",
	// 		time.Since(start), stats.WaitCount)
	// }()

	mysqlstr := fmt.Sprintf("SELECT * FROM %v %v", tableName, where)

	sqlstr := genSelectItem(model, tableName, where)

	var results []T
	err := db.Select(&results, sqlstr)
	if err != nil {
		return nil, mysqlstr, fmt.Errorf("表 %s: %v", tableName, err)
	}

	return results, mysqlstr, nil
}

///////////////////

// BatchInsert 通用批量插入函数
func InsertTable[T any](db *sqlx.DB, tableName string, model []T, skipid ...string) (int64, string, error) {
	if len(model) == 0 {
		return 0, "", nil
	}

	// 获取第一个记录的结构信息
	t := reflect.TypeOf(model[0])
	if t.Kind() == reflect.Ptr {
		t = t.Elem()
	}

	// 可指定过滤的字段名,有的库可能会使用自增的id,因此要过滤
	getskip := func(id string) bool {
		for i := range skipid {
			if skipid[i] == id {
				return true
			}
		}
		return false
	}

	// 收集所有 db tag(排除 id 字段)
	var fieldNames []string
	var dbTags []string

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		dbTag := field.Tag.Get("db")

		if dbTag != "" && dbTag != "-" && !getskip(dbTag) {
			fieldNames = append(fieldNames, field.Name)
			dbTags = append(dbTags, dbTag)
		}
	}

	// 构建带命名参数的 SQL
	var namedParams []string
	for _, tag := range dbTags {
		namedParams = append(namedParams, ":"+tag)
	}

	query := fmt.Sprintf(`
        INSERT INTO %s (%s)
        VALUES (%s)
    `, tableName,
		strings.Join(dbTags, ", "),
		strings.Join(namedParams, ", "))

	// 执行批量插入
	result, err := db.NamedExec(query, model)
	if err != nil {
		return 0, query, fmt.Errorf("表 %s: %v", tableName, err)
	}

	// fmt.Printf("debug insert: [%v]\n", query)

	rows, err := result.RowsAffected()
	if err != nil {
		return int64(len(model)), query, nil // 有些数据库可能不支持
	}

	return rows, query, nil
}

// ////////////////
// QueryCount 查询记录数量
func QueryCount(db *sqlx.DB, tableName, where string, args ...interface{}) (int64, error) {
	sqlstr := fmt.Sprintf("SELECT COUNT(*) FROM %s", tableName)
	if where != "" {
		sqlstr += " WHERE " + where
	}

	var count int64
	err := db.Get(&count, sqlstr, args...)
	if err != nil {
		return 0, fmt.Errorf("表 %s: %v", tableName, err)
	}

	return count, nil
}

测试

建表打印信息:

复制代码
debug 创建表语句:[
                CREATE TABLE IF NOT EXISTS result_data (
                        id TEXT PRIMARY KEY,
                num INTEGER,
                etype INTEGER,
                timestamp TEXT,
                mCode TEXT,
                mName TEXT,
                age TEXT,
                addr TEXT,
                mac TEXT,
                remark TEXT
                )
        ]

插入表打印信息:

复制代码
debug insert: [
        INSERT INTO result_data (id, num, etype, timestamp, mCode, mName, age, addr, mac, remark)
        VALUES (:id, :num, :etype, :timestamp, :mCode, :mName, :age, :addr, :mac, :remark)
    ]

数据表内容:

查表打印信息:

复制代码
[2025-12-30 20:29:23 866] [INFO] query result:  id_8 2025-12-30T20:29:23
[2025-12-30 20:29:23 867] [INFO] query results:
[2025-12-30 20:29:23 868] [INFO] id_0 2025-12-30T20:29:23
[2025-12-30 20:29:23 868] [INFO] id_1 2025-12-30T20:29:23
[2025-12-30 20:29:23 868] [INFO] id_2 2025-12-30T20:29:23
[2025-12-30 20:29:23 869] [INFO] id_3 2025-12-30T20:29:23
[2025-12-30 20:29:23 869] [INFO] id_4 2025-12-30T20:29:23
[2025-12-30 20:29:23 869] [INFO] id_5 2025-12-30T20:29:23
[2025-12-30 20:29:23 870] [INFO] id_6 2025-12-30T20:29:23
[2025-12-30 20:29:23 871] [INFO] id_7 2025-12-30T20:29:23
[2025-12-30 20:29:23 871] [INFO] id_8 2025-12-30T20:29:23

有插入数据的写表打印信息:

复制代码
[2025-12-30 20:30:16 233] [INFO] insert sqlite failed:  表 result_data: constraint failed: UNIQUE constraint failed: result_data.id (1555)
[2025-12-30 20:30:16 234] [INFO] query result:  id_8 2025-12-30T20:29:23
[2025-12-30 20:30:16 234] [INFO] query results:
[2025-12-30 20:30:16 235] [INFO] id_0 2025-12-30T20:29:23
[2025-12-30 20:30:16 236] [INFO] id_1 2025-12-30T20:29:23
[2025-12-30 20:30:16 236] [INFO] id_2 2025-12-30T20:29:23
[2025-12-30 20:30:16 236] [INFO] id_3 2025-12-30T20:29:23
[2025-12-30 20:30:16 237] [INFO] id_4 2025-12-30T20:29:23
[2025-12-30 20:30:16 237] [INFO] id_5 2025-12-30T20:29:23
[2025-12-30 20:30:16 237] [INFO] id_6 2025-12-30T20:29:23
[2025-12-30 20:30:16 238] [INFO] id_7 2025-12-30T20:29:23
[2025-12-30 20:30:16 238] [INFO] id_8 2025-12-30T20:29:23

小结

本文通过示例工程展示了sqlx操作sqlite数据库的方法,理论上只要数据库驱动合适,sqlx可以对任意数据库进行操作,在代码统一了方法,便于维护。

相关推荐
老邓计算机毕设2 小时前
SSM学生选课系统xvbna(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·学生选课系统·ssm 框架·高校教学管理
枷锁—sha3 小时前
【PortSwigger Academy】SQL 注入绕过登录 (Login Bypass)
数据库·sql·学习·安全·网络安全
逍遥德5 小时前
PostgreSQL 中唯一约束(UNIQUE CONSTRAINT) 和唯一索引(UNIQUE INDEX) 的核心区别
数据库·sql·postgresql·dba
钟离墨笺5 小时前
Go语言--2go基础-->基本数据类型
开发语言·前端·后端·golang
工业甲酰苯胺5 小时前
字符串分割并展开成表格的SQL实现方法
数据库·sql
shhpeng5 小时前
go gtk 开发入门
golang
科技块儿5 小时前
IP定位技术:游戏反外挂体系中的精准识别引擎
数据库·tcp/ip·游戏
衫水5 小时前
[特殊字符] MySQL 常用指令大全
数据库·mysql·oracle
卓怡学长5 小时前
m115乐购游戏商城系统
java·前端·数据库·spring boot·spring·游戏
小句6 小时前
SQL中JOIN语法详解 GROUP BY语法详解
数据库·sql