目前的问题是:在插入文章的同时直接传入包含多个 Tag 的切片,由于 GORM 在保存多对多关联时默认认为每个 Tag 都是新的记录,导致每次插入文章时相同的 Tag 都会重新创建。为了解决这个问题,我们需要确保在保存文章之前,先对每个 Tag 去重,也就是先查询数据库中是否已经存在该 Tag,如果存在则使用已有记录,否则再创建新记录。下面给出一种常见的做法,使用 GORM 的事务(Transaction)和 FirstOrCreate 方法来处理这些逻辑。

假设 Tag 模型类似于:

type Tag struct {
    ID   uint   `gorm:"primaryKey"`
    Name string `gorm:"uniqueIndex"` // 保证 tag 名称的唯一性
    // 其它字段...
}

下面是修改后的示例代码:

func CreatePost(article Article) error {
    db := sql.GetDB()

    // 使用事务确保操作的原子性
    return db.Transaction(func(tx *gorm.DB) error {
        // 处理每个 Tag
        for i, tag := range article.Tags {
            var existingTag Tag
            // 根据 Tag 的唯一字段(例如 Name)进行查找
            if err := tx.Where("name = ?", tag.Name).First(&existingTag).Error; err != nil {
                if errors.Is(err, gorm.ErrRecordNotFound) {
                    // 如果没有找到,则创建新标签
                    if err := tx.Create(&tag).Error; err != nil {
                        return err
                    }
                    // 更新 article.Tags 中的数据为新创建的 tag
                    article.Tags[i] = tag
                } else {
                    // 出现其它错误时,返回错误,事务会回滚
                    return err
                }
            } else {
                // 找到了,使用已有的 tag 记录
                article.Tags[i] = existingTag
            }
        }

        // 创建文章,GORM 会自动将 article.Tags 中的 Tag 与 Article 关联(写入中间表)
        if err := tx.Create(&article).Error; err != nil {
            return err
        }
        return nil
    })
}

说明

  1. 事务使用
    使用 db.Transaction 来确保整个操作的原子性,如果中间任一步出现错误,整个事务会回滚。
  2. Tag 去重逻辑
    对于每个传入的 Tag,我们先根据唯一字段(比如 Name)进行查找:

    • 如果找不到则使用 tx.Create(&tag) 新建标签;
    • 如果找到了,则直接使用已存在的标签,避免重复插入。
  3. 自动建立关联
    当调用 tx.Create(&article) 时,GORM 会自动处理多对多关系,将 article.Tags 中的 Tag 信息写入关联表 article_tags。注意这要求你的 Article 模型中所定义的多对多标签 gorm:"many2many:article_tags;" 是正确设置的。

通过这种方式,每次插入文章时,相同的 Tag 都只会在数据库中保留一条记录,而只是在关联表中添加一条关联记录。这样既避免了数据冗余,也能保持多对多关系的正确性。

直接调用 db.Delete(&article) 仅会删除 articles 表中的记录,而不会自动清除多对多关联关系(即 article_tags 连接表中的记录)。

在 GORM 中,对于多对多关联,如果需要同时删除关联关系,需要使用 Association Mode 来额外处理,例如:

// 删除文章所有 Tag 关联(删除连接表中的记录)
db.Model(&article).Association("Tags").Clear()

或者,可以配置数据库中的外键约束(例如添加 ON DELETE CASCADE),不过这需要在数据库层面配置,是另一种实现方式。

什么是回调函数?

回调函数是一种编程模式,指的是将一个函数作为参数传递给另一个函数,并在特定条件或事件发生时由后者调用前者。回调函数允许异步操作和事件驱动的编程。

简单来说:回调函数是你定义好但由别人(通常是系统或其他函数)在适当时候调用的函数。

回调函数的主要特点

  1. 函数作为参数:回调函数通常作为参数传递给另一个函数
  2. 延迟执行:回调函数不会立即执行,而是在特定条件满足时执行
  3. 异步处理:常用于处理异步操作的结果
  4. 解耦:将调用者与被调用者分离,提高代码灵活性

回调函数的例子

1. JavaScript中的简单回调

// 定义一个回调函数
function greeting(name) {
    console.log('Hello ' + name);
}

// 定义一个接收回调的函数
function processUserInput(callback) {
    const name = prompt('Please enter your name.');
    callback(name); // 在适当时候调用回调
}

// 使用回调
processUserInput(greeting);

2. Node.js中的文件读取(异步I/O)

const fs = require('fs');

// 回调函数
function fileReadCallback(err, data) {
    if (err) {
        console.error('Error reading file:', err);
        return;
    }
    console.log('File content:', data);
}

// 异步读取文件,完成后调用回调
fs.readFile('example.txt', 'utf8', fileReadCallback);

3. 事件监听(浏览器DOM事件)

// 回调函数
function handleClick(event) {
    console.log('Button clicked!', event);
}

// 注册回调
document.getElementById('myButton').addEventListener('click', handleClick);

4. 排序中的比较回调

const numbers = [3, 1, 4, 1, 5, 9, 2, 6];

// 使用回调函数定义排序规则
numbers.sort(function(a, b) {
    return a - b; // 升序排序
});

console.log(numbers); // [1, 1, 2, 3, 4, 5, 6, 9]

回调函数的优缺点

优点

  • 实现异步编程
  • 提高代码复用性
  • 解耦调用者和被调用者
  • 适用于事件驱动编程

缺点

  • 回调地狱(Callback Hell):多层嵌套回调使代码难以阅读和维护
  • 错误处理复杂:需要在每个回调中单独处理错误
  • 控制流不直观

回调地狱示例

// 多层嵌套的回调,难以阅读和维护
doSomething(function(result) {
    doSomethingElse(result, function(newResult) {
        doThirdThing(newResult, function(finalResult) {
            console.log('Got the final result:', finalResult);
        }, failureCallback);
    }, failureCallback);
}, failureCallback);

现代JavaScript通常使用Promise或async/await来解决回调地狱问题。

回调函数是编程中非常重要的概念,特别是在JavaScript和Node.js中广泛使用。理解回调函数是掌握异步编程的基础。

缓存问题

  • Git 会缓存跟踪状态,即使添加了忽略规则,已跟踪的文件仍会被继续跟踪
  • 解决方案:

    git rm -r --cached /backend/tmp/
    git add .
    git commit -m "停止跟踪 tmp 目录"

安装

go get github.com/joho/godotenv

// go install github.com/joho/godotenv/cmd/godotenv@latest

使用

将应用程序配置添加到项目根目录中的 .env 文件中:

S3_BUCKET=YOURS3BUCKET
SECRET_KEY=YOURSECRETKEYGOESHERE

然后,在Go 应用程序中,可以执行以下作

package main

import (
    "log"
    "os"

    "github.com/joho/godotenv"
)

func main() {
  err := godotenv.Load()
  if err != nil {
    log.Fatal("Error loading .env file")
  }

  s3Bucket := os.Getenv("S3_BUCKET")
  secretKey := os.Getenv("SECRET_KEY")

  // now do something with s3 or whatever
}

参考文档:https://github.com/joho/godotenv