跳到主要内容

2 篇博文 含有标签「mysql」

查看所有标签

· 阅读需 5 分钟

记录下记账项目后端的 orm 工具 Gorm 的基本使用

1 连接数据库

在 database package 暴露数据库实例 DB。声明 ConnectDB 方法,内部使用 gorm.Open 连接数据库,并给 DB 赋值

package database

var DB *gorm.DB

func ConnectDB() {
if DB != nil {
return
}

// 从环境变量中获取数据库连接字符串
dsn := os.Getenv("DB_DSN")
if dsn == "" {
log.Fatal("DB_DSN is not set")
}

// 连接数据库
database, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

if err != nil {
log.Fatal("Fail to connect database!", err)
}

// 赋值 DB
DB = database
}

2 model 定义

https://gorm.io/zh_CN/docs/models.html

model 就是一个结构体,定义类型,后跟注释形式的字段配置,例如 gorm:"<-:create" json:"createdAt"

type ValidationCode struct {
ID uint `gorm:"primaryKey" json:"id"`
Email string `gorm:"size:255;not null" json:"email"`
Code string `gorm:"size:255;not null" json:"code"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"`
UpdatedAt time.Time `gorm:"<-:update" json:"updatedAt"`
CreatedAt time.Time `gorm:"<-:create" json:"createdAt"`
UsedAt *time.Time `json:"usedAt"`
}
  • 一对多

一个 user 可以创建多个 tag 和 item 使用外键约束,关联 tag 和 user 的关系即可

type User struct {
gorm.Model
Email string `gorm:"size:255;not null;unique"`
Tags []Tag
Items []Item
}
type Tag struct {
gorm.Model
UserID uint `gorm:"not null;index"`
// ...
// 只要遵守约定 模型名+ID 那么
// GORM 能够自动识别这种外键关系,不需要显式地使用 foreignKey 标签指定。
User User `gorm:"foreignKey:UserID"`
}
  • 多对多

一个 item 可以属于多个 tag,同时一个 tag 下也可以有很多 item

使用连接表来定义多对多的关系,这种关系声明在一个表的一个字段就可以

会自动生成一个连接表

type Tag struct {
gorm.Model
UserID uint `gorm:"not null;index"`
User User `gorm:"foreignKey:UserID"`
}
type Item struct {
gorm.Model
UserID uint `gorm:"not null;index"`
User User `gorm:"foreignKey:UserID"`
// item_tags 多对多连接表的结构通常是由 GORM 自动生成的
// 它会包含两个字段:item_id 和 tag_id 分别作为外键指向 items 表和 tags 表的主键
// 通常,在处理数据库和关联关系时,推荐使用指针类型作为切片的元素类型,因为这可以更好地与ORM工作,并方便处理没有值(nil)的情形。因此推荐将 item 中的 Tags 定义为指向 Tag 的指针的切片
Tags []*Tag `gorm:"many2many:item_tags;"`
}

若要重写外键,可以使用标签foreignKeyreferencesjoinforeignKeyjoinReferences。当然,您不需要使用全部的标签,你可以仅使用其中的一个重写部分的外键、引用,例如

Tags []Tag `gorm:"many2many:item_tags;foreignKey:ID;joinForeignKey:ItemID;References:ID;joinReferences:TagID"`
  • foreignKey:ID 指的是Item模型的ID字段作为连接表(item_tags)的外键。

  • joinForeignKey:ItemID 指的是在连接表中用于指向Item记录的字段名。

  • References:ID 指的是Tag模型中的ID字段作为参照。

  • joinReferences:TagID 指的是在连接表中用于指向Tag记录的字段名。

相当于如下 sql

CREATE TABLE `items` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL,
`user_id` BIGINT,
`created_at` DATETIME,
`updated_at` DATETIME,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
CREATE TABLE `item_tags` (
`item_id` BIGINT,
`tag_id` BIGINT,
PRIMARY KEY (`item_id`, `tag_id`),
FOREIGN KEY (`item_id`) REFERENCES `items`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`tag_id`) REFERENCES `tags`(`id`) ON DELETE CASCADE
);

3 同步数据库

使用 cobra 封装为一个指令

func Migrate() {
if DB != nil {
// 迁移数据库
err := DB.AutoMigrate(&User{}, &Item{}, &Tag{}, &ValidationCode{})
if err != nil {
log.Fatal("Fail to migrate database!", err)
}
// 创建测试用户
DB.Save(&User{
Email: "test@test.com",
})
}
}

4 清空数据库

主要是关闭外键检查,方便测试

func TruncateTables(t *testing.T, tables []string) {
// 禁用外键检查
err := DB.Exec("SET FOREIGN_KEY_CHECKS=0;").Error
if err != nil {
if t != nil {
t.Fatalf("Failed to disable foreign key checks: %v", err)
} else {
log.Fatalf("Failed to disable foreign key checks: %v", err)
}
}

// 清空所有给定的表
for _, table := range tables {
if err = DB.Exec("TRUNCATE TABLE " + table + ";").Error; err != nil {
if t != nil {
t.Fatalf("Failed to truncate table %s: %v", table, err)
} else {
log.Fatalf("Failed to truncate table %s: %v", table, err)
}
}
}

// 重新启用外键检查
err = DB.Exec("SET FOREIGN_KEY_CHECKS=1;").Error
if err != nil {
if t != nil {
t.Fatalf("Failed to enable foreign key checks: %v", err)
} else {
log.Fatalf("Failed to enable foreign key checks: %v", err)
}
}
}

· 阅读需 11 分钟

需求

批量更新或新增数据,输入一个原始数据的数组,根据数据中在数据库中是否存在来判断是更新还是修改

直接想到的做法就是遍历这个数组,然后逐个查询,如果查到了,就修改,反之则新增

const db = client.db("数据库名").collection("users")
const data = [
{ code: '1', name: 'a', qty: 100 },
// ...
]

for (const d of data) {
// 先用 find 去查找数据是否存在
const result = await db.findOne({ code: d.code })
if (result) {
// 如果存在,则更新
await db.updateOne({ code: d.code }, d)
}else {
// 如果不存在,则新增
await db.insertOne(d)
}
}

虽然思路很清晰,但当数据量大的时候,频繁操作数据库可能会引起一些性能问题

相比逐条处理数据,批量更新通常会带来更高的性能

可以利用 mongoDB 提供的一些批量操作 api,一次性更新多个文档

方案1 预处理

  1. 先遍历查询每条数据
  2. 数据预处理:维护一个 insertDocsupdateDocs
  3. 最后分别调用 updateManyinsertMany 来批量更新这些数据
    • 注意 updateMany 用于同时更新多个满足特定条件的文档,但是它并不适用于每个文档有不同更新条件的情况
    • 所以这里使用 bulkWrite 来批量更新
const data = [
{ code: '1', name: 'a', qty: 100 },
// ...
]

const updateDocs = []
const insertDocs = []

for (let d of data) {
const result = await db.findOne({ code: d.code })
if (result) {
updateDocs.push({
filter: { code: d.code },
update: { $set: d }
})
} else {
insertDocs.push(d)
}
}

// 批量新增
const insertResult = await db.insertMany(insertDocs)
console.log(`Inserted ${insertResult.insertedCount} new document(s).`)

// 批量更新
const bulkUpdateOps = updateDocs.map(update => ({
updateOne: {
filter: update.filter,
update: update.update,
upsert: false
}
}));
const updateResult = await collection.bulkWrite(bulkUpdateOps)
console.log(`${updateResult.matchedCount} document(s) matched the filter`)
console.log(`${updateResult.modifiedCount} document(s) were updated`)

方案2 bulkWrite

bulkWrite API 原生支持批量新增或更新的操作,利用其 upsert 配置的特性来完成这个需求

const data = [
{ code: '1', name: 'a', qty: 100 },
// ...
]
// 创建 bulkWrite 操作数组
const bulkOperations = data.map(d => ({
updateOne: {
filter: { code: d.code },
update: { $set: d },
// upsert 表示新增或更新
upsert: true // 如果不存在,那么就新增
}
}))

// 执行 bulkWrite 操作
const result = await db.bulkWrite(bulkOperations)

console.log(`${result.matchedCount} document(s) matched the query criteria.`)
console.log(`${result.modifiedCount} document(s) was/were updated.`)
console.log(`${result.upsertedCount} document(s) was/were inserted.`)
console.log(`Upserted document _id: ${result.upsertedIds}`)

批量更新

类似于 MongoDB 中的批量更新功能,关系型数据库如 MySQL 和 PostgreSQL 也提供了处理批量更新的能力。不过,批量更新的实现方式和语法因不同的数据库而异。以下是 MySQL 和 PostgreSQL 中批量更新数据的基本示例:

MySQL 批量更新

在 MySQL 中,你可以使用 INSERT ... ON DUPLICATE KEY UPDATE 语法来达到类似批量更新的效果。这要求表中有唯一索引或主键,当插入的行与现有行在唯一索引或主键上冲突时,则执行更新操作。

INSERT INTO 表名 (1,2, ...)
VALUES
(1a,2a, ...),
(1b,2b, ...),
...
ON DUPLICATE KEY UPDATE
1 = VALUES(1),2 = VALUES(2), ...;

PostgreSQL 批量更新

在 PostgreSQL 中,可以使用 INSERT ... ON CONFLICT 语法实现类似的功能。这同样依赖于表中的唯一约束或主键。

INSERT INTO 表名 (1,2, ...)
VALUES
(1a,2a, ...),
(1b,2b, ...),
...
ON CONFLICT (列名) DO UPDATE
SET1 = EXCLUDED.1,2 = EXCLUDED.2, ...;

在这两个示例中,批量更新(或称为“批量插入或更新”)的行为取决于是否有与现有数据冲突的新数据。如果有冲突,则执行更新;如果没有冲突,则插入新数据。

这些方法非常适合于需要同步大量数据或进行批量数据处理的场景。需要注意的是,具体的语法和实现可能因数据库的版本和具体配置而有所不同,因此在实际应用中应参考相关数据库的官方文档。

关联查询

mongoDB 聚合查询

MongoDB 作为一个非关系型数据库(NoSQL),其原生的设计并没有传统意义上的“连表查询”概念。在关系型数据库(如 MySQL)中,多个表通过关联字段进行关联查询(JOIN 操作),而 MongoDB 中存储数据的基本单位是集合(Collection),它们之间不像关系型数据库中的表那样自然具有关系。

然而,MongoDB 提供了 $lookup 聚合管道操作符,它允许你在聚合查询中实现类似于 SQL 中的 JOIN 功能。使用 $lookup,你可以引用其他集合的文档并将它们与当前集合的文档汇聚在一起。

以下是一个 $lookup 的图书和作者示例,模拟关系型数据库中的连表查询:

db.books.aggregate([
{
$lookup: {
from: "authors", // 要 join 的集合名
localField: "authorId", // 本集合中用于 join 的字段
foreignField: "_id", // 外部集合中用于 join 的字段
as: "authorInfo" // 查找到的数据放在哪个字段
}
}
]);

在这个例子中,我们假设 books 集合中的每本书都有一个 authorId 字段,它引用了 authors 集合中对应作者的 _id 字段。$lookup 会将 authors 集合中匹配到的作者文档添加到 authorInfo 字段中。

在 MongoDB 3.2 以上版本中,$lookup 可以非常有效地执行这种操作,使 MongoDB 更接近于传统的关系型数据库体验。

此外,MongoDB 3.6 以上版本引入了更高级的 $lookup 语法,允许进行更复杂的连接操作,包括可以实现类似左外连接的操作。

尽管 MongoDB 有 $lookup 操作符,但它并不是为了大量连表操作而设计的。在使用 MongoDB 设计数据模型时,通常推荐的做法是尽量减少需要连接操作的场景,比如通过嵌入文档(embedding)或使用文档引用(referencing)来优化查询效率。

公司规定数据库每次关联查询不能超过三张表,怎么做

使用中间表减少连接

假设我们有三个表:users (用户信息)、orders (订单信息)、和 products (产品信息)。为了减少查询时的连接数量,我们可能会创建一个中间表 order_details,其中包含了用户信息和订单信息的部分数据。

创建中间表 order_details

CREATE TABLE order_details AS
SELECT u.user_id, u.user_name, o.order_id, o.product_id, p.product_name, o.quantity
FROM users u
JOIN orders o ON u.user_id = o.user_id
JOIN products p ON o.product_id = p.product_id;

之后的查询可以直接从 order_details 中获取数据,无需再次进行三表连接。

分解查询

假设我们需要基于用户的订单来获取产品信息。我们可以先查询用户的订单,然后使用这些订单ID来查询订单中的产品信息。

  1. 查询用户订单:
SELECT order_id
FROM orders
WHERE user_id = '123';

假设这返回了订单ID列表 [456, 789]

  1. 基于订单ID查询产品信息:
SELECT product_name, quantity
FROM order_details
WHERE order_id IN (456, 789);

这样,我们就避免了一次性连接多个表。

示例3: 使用子查询

子查询可以在单个 SQL 语句中嵌入另一个查询,从而减少同时需要连接的表的数量。

查询订单中的产品信息,其中产品价格大于某个值:

SELECT o.order_id, (SELECT product_name FROM products p WHERE p.product_id = o.product_id AND p.price > 100) as expensive_products 
FROM orders o
WHERE o.user_id = '123';

这里,我们在SELECT子句中使用了子查询来获取符合条件的产品名称。

示例4: 使用公用表表达式(CTE)

查找过去一月内活跃用户的订单:

WITH ActiveUsers AS (
SELECT user_id
FROM users
WHERE last_login_date > CURRENT_DATE - INTERVAL '1 month'
),
UserOrders AS (
SELECT user_id, order_id
FROM orders
WHERE user_id IN (SELECT user_id FROM ActiveUsers)
)
SELECT u.user_name, o.order_id
FROM UserOrders o
JOIN users u ON u.user_id = o.user_id;

这里,我们使用了两个CTE(ActiveUsers 和 UserOrders)来逐步减少所需连接的表数量。

示例5: 优化数据模型

在设计数据库时,可以通过合理的数据模型设计来避免复杂的查询,例如通过适当的反归一化来预存一些经常一起查询的数据字段,或者根据查询模式调整表结构。

例如:在 orders 表中添加 user_name 字段,避免查询时每次都要连接 users 表来获取用户名。