登录 注册

 golang在postgresql数据库中如何查询记录是否存在,不存在保存

2025-11-02  回复(0) 

在 Golang 中查询 PostgreSQL 数据库记录是否存在,如果不存在则保存,通常有以下几种方法,每种方法都有其优缺点,选择哪种方法取决于你的具体需求和数据库的负载情况。

核心思路:

1. 查询: 使用 SELECT 语句来检查记录是否存在。
2. 判断: 根据查询结果判断记录是否存在。
3. 插入/更新:
* 如果不存在,则执行 INSERT 语句保存新记录。
* 如果存在,可以根据需求选择跳过、更新或抛出错误。

常用方法:

方法一:先查询,再根据结果插入 (最直接,但可能存在竞态条件)


这是最直观的方法。先执行一个 SELECT 查询,然后根据查询结果来决定是插入还是不做操作。

go
package main

import (
"database/sql"
"fmt"
"log"

_ "github.com/lib/pq" // PostgreSQL driver
)

type User struct {
ID int
Name string
Email string
}

func main() {
// 替换成你的数据库连接字符串
connStr := "user=youruser password=yourpassword dbname=yourdb host=localhost port=5432 sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
defer db.Close()

// 确保数据库连接可用
err = db.Ping()
if err != nil {
log.Fatal(err)
}

userToSave := User{Name: "Alice", Email: "alice@example.com"}

// 1. 查询记录是否存在
var exists bool
// 这里我们查询一个能够唯一标识用户(例如 Email)的字段
query := "SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)"
err = db.QueryRow(query, userToSave.Email).Scan(&exists)
if err != nil {
log.Fatalf("Error checking existence: %v", err)
}

// 2. 判断并插入
if !exists {
// 记录不存在,执行插入操作
insertSQL := `INSERT INTO users (name, email) VALUES ($1, $2)`
_, err := db.Exec(insertSQL, userToSave.Name, userToSave.Email)
if err != nil {
log.Fatalf("Error inserting user: %v", err)
}
fmt.Printf("User '%s' inserted successfully.\n", userToSave.Name)
} else {
fmt.Printf("User with email '%s' already exists. Not inserting.\n", userToSave.Email)
}
}


优点:

* 逻辑清晰,容易理解。

缺点:

* 竞态条件 (Race Condition): 这是最主要的缺点。如果在查询和插入之间,另一个并发的请求也插入了相同的记录,那么你的插入操作将会失败(如果设置了唯一性约束)。虽然你的代码在 if !exists 块中执行插入,但在这个时间窗口内,可能已经有人插入了。
* 性能: 需要两次数据库往返(一次查询,一次插入),如果高并发,性能会有影响。

方法二:使用 INSERT ... ON CONFLICT (推荐,PostgreSQL 特有)


PostgreSQL 提供了 INSERT ... ON CONFLICT(在 PostgreSQL 9.5+ 中引入)子句,这是处理“如果不存在则插入,否则忽略或更新”场景的最优雅、最高效、最安全的方法。

你可以定义一个唯一性约束(通常在 emailid 字段上),然后利用 ON CONFLICT DO NOTHING 来实现“如果冲突则什么也不做”。

前提:

* 在 users 表的 email 字段上需要有一个 UNIQUE 约束。

sql
-- SQL for creating a unique constraint
CREATE UNIQUE INDEX IF NOT EXISTS users_email_unique ON users (email);


go
package main

import (
"database/sql"
"fmt"
"log"

_ "github.com/lib/pq" // PostgreSQL driver
)

type User struct {
ID int
Name string
Email string
}

func main() {
// 替换成你的数据库连接字符串
connStr := "user=youruser password=yourpassword dbname=yourdb host=localhost port=5432 sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
defer db.Close()

err = db.Ping()
if err != nil {
log.Fatal(err)
}

userToSave := User{Name: "Bob", Email: "bob@example.com"}

// 使用 INSERT ... ON CONFLICT DO NOTHING
// 'users_email_unique' 是你定义的唯一性索引的名称
insertSQL := `INSERT INTO users (name, email) VALUES ($1, $2) ON CONFLICT ON CONSTRAINT users_email_unique DO NOTHING`
// 如果你只想基于 email 字段来冲突检测,可以简化为 ON CONFLICT (email) DO NOTHING
// insertSQL := `INSERT INTO users (name, email) VALUES ($1, $2) ON CONFLICT (email) DO NOTHING`

result, err := db.Exec(insertSQL, userToSave.Name, userToSave.Email)
if err != nil {
log.Fatalf("Error executing INSERT ON CONFLICT: %v", err)
}

rowsAffected, err := result.RowsAffected()
if err != nil {
log.Fatalf("Error getting rows affected: %v", err)
}

if rowsAffected > 0 {
fmt.Printf("User '%s' inserted successfully.\n", userToSave.Name)
} else {
fmt.Printf("User with email '%s' already exists. Insertion skipped.\n", userToSave.Email)
}
}


优点:

* 原子性: INSERT ... ON CONFLICT 是一个原子操作,完美解决了竞态条件问题。
* 性能: 通常比先查询再插入的性能更好,因为它只需要一次数据库往返。
* 简洁: SQL 语句更简洁。
* PostgreSQL 特有: 只能在 PostgreSQL 中使用。

缺点:

* 需要数据库支持(PostgreSQL 9.5+)。
* 需要在需要进行冲突检测的字段上创建 UNIQUE 约束。

方法三:使用 INSERT ... ON CONFLICT ... DO UPDATE (如果存在则更新)


如果你希望在记录存在时,不是简单地忽略,而是更新现有记录,可以使用 ON CONFLICT ... DO UPDATE

go
package main

import (
"database/sql"
"fmt"
"log"

_ "github.com/lib/pq" // PostgreSQL driver
)

type User struct {
ID int
Name string
Email string
}

func main() {
// 替换成你的数据库连接字符串
connStr := "user=youruser password=yourpassword dbname=yourdb host=localhost port=5432 sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
defer db.Close()

err = db.Ping()
if err != nil {
log.Fatal(err)
}

userToUpdateOrInsert := User{Name: "Charlie", Email: "charlie@example.com"}

// 使用 INSERT ... ON CONFLICT DO UPDATE
// 'users_email_unique' 是你定义的唯一性索引的名称
// EXCLUDED.* 表示被排除(即尝试插入但失败)的行的所有列
insertOrUpdateSQL := `
INSERT INTO users (name, email)
VALUES ($1, $2)
ON CONFLICT ON CONSTRAINT users_email_unique
DO UPDATE SET name = EXCLUDED.name
`
// 如果你只想基于 email 字段来冲突检测,可以简化为 ON CONFLICT (email) DO UPDATE SET ...
// insertOrUpdateSQL := `
// INSERT INTO users (name, email)
// VALUES ($1, $2)
// ON CONFLICT (email)
// DO UPDATE SET name = EXCLUDED.name
// `

result, err := db.Exec(insertOrUpdateSQL, userToUpdateOrInsert.Name, userToUpdateOrInsert.Email)
if err != nil {
log.Fatalf("Error executing INSERT ON CONFLICT DO UPDATE: %v", err)
}

rowsAffected, err := result.RowsAffected()
if err != nil {
log.Fatalf("Error getting rows affected: %v", err)
}

if rowsAffected > 0 {
// 注意:如果记录已存在并被更新,rowsAffected 也会是 1。
// 区分插入和更新需要更复杂的逻辑,比如返回 RETURNING ...
fmt.Printf("User with email '%s' processed. (Inserted or Updated)\n", userToUpdateOrInsert.Email)
} else {
// 这种情况不太可能发生,除非 ON CONFLICT DO UPDATE 的 SET 子句没有修改任何东西
fmt.Printf("User with email '%s' already exists, but no update was performed.\n", userToUpdateOrInsert.Email)
}
}


优点:

* 原子性: 解决竞态条件。
* 性能: 一次数据库往返。
* 功能性: 允许在记录存在时执行更新操作。

缺点:

* PostgreSQL 特有。
* 区分插入和更新: result.RowsAffected() 在插入和更新时都可能返回 1。要精确知道是插入还是更新,需要使用 RETURNING 子句,这会增加 SQL 的复杂度。

方法四:使用 INSERT ... RETURNING 和错误捕获 (适用于 PostgreSQL < 9.5 或其他数据库)


如果你使用的是不支持 ON CONFLICT 的 PostgreSQL 版本,或者想在其他数据库中使用类似逻辑,可以结合 INSERTRETURNING 子句,然后捕获 PostgreSQL 的错误(例如 unique_violation)。

前提:

* 在 users 表的 email 字段上需要有一个 UNIQUE 约束。

go
package main

import (
"database/sql"
"errors"
"fmt"
"log"

"github.com/lib/pq" // PostgreSQL driver
)

type User struct {
ID int
Name string
Email string
}

func main() {
// 替换成你的数据库连接字符串
connStr := "user=youruser password=yourpassword dbname=yourdb host=localhost port=5432 sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
defer db.Close()

err = db.Ping()
if err != nil {
log.Fatal(err)
}

userToSave := User{Name: "David", Email: "david@example.com"}

// 尝试插入,并使用 RETURNING id 来获取插入的 ID
insertSQL := `INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id`
var insertedID int

err = db.QueryRow(insertSQL, userToSave.Name, userToSave.Email).Scan(&insertedID)

if err != nil {
// 检查错误是否是 PostgreSQL 的 unique_violation 错误
if pgErr, ok := err.(*pq.Error); ok && pgErr.Code == "23505" {
// 记录已存在,可以忽略或进行其他操作
fmt.Printf("User with email '%s' already exists. Insertion skipped.\n", userToSave.Email)
} else {
// 其他数据库错误
log.Fatalf("Error executing INSERT: %v", err)
}
} else {
// 插入成功
fmt.Printf("User '%s' inserted successfully with ID: %d\n", userToSave.Name, insertedID)
}
}


优点:

* 原子性: 错误捕获机制可以处理并发情况。
* 灵活性: 适用于不支持 ON CONFLICT 的数据库,并且可以处理插入失败的情况。
* 获取插入 ID: RETURNING 子句非常有用。

缺点:

* PostgreSQL 特有: pq.Error 是特定于 github.com/lib/pq 驱动的。
* 错误处理: 需要仔细处理各种数据库错误。
* 性能: 相对于 ON CONFLICT DO NOTHING,可能稍慢,因为它涉及错误判断。

总结与推荐:

1. 首选 INSERT ... ON CONFLICT DO NOTHING (方法二): 如果你使用的是 PostgreSQL 9.5+,并且目标是“如果记录不存在则插入,否则忽略”,这是最推荐的方法。它最安全、最高效、最简洁。
2. 次选 INSERT ... ON CONFLICT DO UPDATE (方法三): 如果你需要“如果记录不存在则插入,如果存在则更新”,也请优先考虑此方法。
3. 备选 INSERT ... RETURNING + 错误捕获 (方法四): 如果你的 PostgreSQL 版本较旧,或者需要跨数据库兼容处理“先插入,失败再处理”的逻辑,这是个不错的选择。
4. 避免纯粹的“先查询再插入” (方法一): 除非你的场景对并发性和数据一致性要求不高,否则应尽量避免这种方法,因为它容易引入竞态条件。

在实际应用中,根据你的数据库版本、需求和对性能的要求来选择最合适的方法。 ON CONFLICT 绝对是 PostgreSQL 提供的宝贵特性,应该充分利用。

#回复 AI问答 上传/拍照 我的