数据库驱动库的注册方式
import (
"database/sql"
// 注册 mysql 驱动
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/test")
if err != nil {
panic(err)
}
defer db.Close()
}
在这个例子中,我们使用了 github.com/go-sql-driver/mysql 包,并通过 sql.Open() 函数打开了一个 MySQL 数据库连接。
golang 中 database/sql 都是一套接口定义,并没有具体的实现,程序需要提供具体数据库的驱动实现。比如对于 mysql 是 github.com/go-sql-driver/mysql,对于 postgresql 是 https://github.com/lib/pq。
对于 MySQL 驱动程序 mysql 包来说,虽然我们并没有在代码中直接使用它,但是我们仍然需要在导入列表中将这个包导入进来。这样编译器就可以在编译时找到这个包,并将其链接到我们的二进制文件中,这个行为叫数据库驱动注册。
查询单行数据并绑定字段到变量
func main() {
// 打开数据库连接
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/test")
if err != nil {
panic(err.Error())
}
defer db.Close()
// 查询多个字段
var (
id int
name string
)
if err = db.QueryRow("SELECT id, name FROM users WHERE id = ?", 1).Scan(&id, &name); err != nil {
panic(err.Error())
}
fmt.Printf("ID: %d; Name: %s", id, name)
}
在 main 函数中,首先打开了与 MySQL 数据库的连接。sql.Open() 函数接受两个参数,第一个参数是连接数据库的驱动类型,这里使用了 "mysql";第二个参数是连接数据库的字符串,格式为 "user:password@tcp(localhost:3306)/database",其中 user 和 password 分别对应 MySQL 数据库的用户名和密码,localhost:3306 是 MySQL 数据库所在的地址和端口号,database 是要连接的数据库名称。如果连接失败,则会触发 panic。
在 defer 语句中,将数据库连接 db 关闭。这样保证了在程序运行结束时,数据库连接可以被正确地关闭,而不会出现连接泄漏等问题。
使用 db.QueryRow() 函数查询多个字段,查询语句是 "SELECT id, name FROM users WHERE id = ?",其中 ? 为占位符。这里将 1 作为参数传递给了 QueryRow() 函数,表示查询 id 为 1 的记录。QueryRow() 函数会返回一行数据。然后使用 Scan() 函数将查询结果映射到变量中。这里定义了变量 id 和 name 分别为整型和字符串类型。如果查询出错,则会触发 panic。
查询多行数据并且遍历
func main() {
// 打开数据库连接
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/test")
if err != nil {
panic(err.Error())
}
defer db.Close()
// 查询多行数据
rows, err := db.Query("SELECT id, name FROM users WHERE age > ?", 18)
if err != nil {
panic(err.Error())
}
defer rows.Close()
// 遍历查询结果
for rows.Next() {
var (
id int
name string
)
if err := rows.Scan(&id, &name); err != nil {
panic(err.Error())
}
fmt.Printf("ID: %d; Name: %s\n", id, name)
}
// 检查遍历结果时是否出现错误
if err := rows.Err(); err != nil {
panic(err.Error())
}
}
这个例子使用了 SQL 查询语句 SELECT id, name FROM users WHERE age > ? 查询了 users 表中年龄大于 18 的用户的 id 和 name 字段。通过执行 db.Query() 函数,将查询结果保存在 *sql.Rows 类型的结果集中。然后,使用 rows.Next() 函数遍历查询结果集,检索每一行数据,使用 rows.Scan() 函数将查询结果映射到变量中。如果遍历过程中出现错误,将会触发 panic。最后,如果需要,使用 rows.Err() 函数检查遍历结果时是否出现错误。
需要注意的是,当使用 rows.Next() 函数获取下一行数据时,必须记得在最后调用 rows.Close() 函数,关闭结果集。另外,在处理查询结果时,需要注意打开和关闭数据库连接的时间和生命周期。另外,需要根据实际情况检查 SQL 查询语句中使用的参数是否正确,以及遍历结果时是否出现错误,并对错误进行处理。
插入数据并且获得新记录的主键ID
func main() {
// 打开数据库连接
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/test")
if err != nil {
panic(err.Error())
}
defer db.Close()
// 插入一行数据
result, err := db.Exec("INSERT INTO users(name, age) VALUES(?, ?)", "Alice", 30)
if err != nil {
panic(err.Error())
}
// 获取插入的主键 ID
id, err := result.LastInsertId()
if err != nil {
panic(err.Error())
}
fmt.Printf("Inserted row ID: %d\n", id)
}
代码使用了 SQL 插入语句 INSERT INTO users(name, age) VALUES(?, ?),将一行数据插入到了 users 表中。通过执行 db.Exec() 函数,将插入语句发送到 MySQL 数据库,并返回结果对象 result。如果插入过程中出现错误,将会触发 panic。
接着,使用 result.LastInsertId() 函数获取插入的主键 ID。这个函数返回一个整型变量,表示插入的最后一行数据的主键 ID。如果获取主键 ID 的过程中出现错误,则会触发 panic。最后,使用 fmt.Printf() 函数输出插入的主键 ID。
需要注意的是,插入数据前需要打开数据库连接,并在使用完毕之后关闭数据库连接。此外,需要根据实际情况检查 SQL 查询语句中使用的参数是否正确,以及获取数据时是否出现错误,并对错误进行处理。
根据主键删除一条记录
func main() {
// 打开数据库连接
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/test")
if err != nil {
panic(err.Error())
}
defer db.Close()
// 删除一行记录
result, err := db.Exec("DELETE FROM users WHERE id = ?", 1)
if err != nil {
panic(err.Error())
}
// 获取删除操作影响的行数
rowsAffected, err := result.RowsAffected()
if err != nil {
panic(err.Error())
}
fmt.Printf("Deleted %d rows.\n", rowsAffected)
}
代码使用了 SQL 删除语句 DELETE FROM users WHERE id = ?,根据给定的主键 ID (这里是 1)从 users 表中删除一行记录。通过执行 db.Exec() 函数,将删除语句发送到 MySQL 数据库,并返回结果对象 result。如果删除过程中出现错误,将会触发 panic。
接着,使用 result.RowsAffected() 函数获取删除操作影响的行数。这个函数返回一个整型变量,表示删除操作影响的行数。如果获取影响的行数的过程中出现错误,则会触发 panic。最后,使用 fmt.Printf() 函数输出删除操作影响的行数。
根据主键更新一条记录
func main() {
// 打开数据库连接
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/test")
if err != nil {
panic(err.Error())
}
defer db.Close()
// 更新一行数据
result, err := db.Exec("UPDATE users SET name = ? WHERE id = ?", "Alice", 1)
if err != nil {
panic(err.Error())
}
// 获取更新操作影响的行数
rowsAffected, err := result.RowsAffected()
if err != nil {
panic(err.Error())
}
fmt.Printf("Updated %d rows.\n", rowsAffected)
}
代码使用了 SQL 更新语句 UPDATE users SET name = ? WHERE id = ?,将 users 表中主键 ID 为 1 的记录的 name 字段更新为 "Alice"。通过执行 db.Exec() 函数,将更新语句发送到 MySQL 数据库,并返回结果对象 result。如果更新过程中出现错误,将会触发 panic。
接着,使用 result.RowsAffected() 函数获取更新操作影响的行数。这个函数返回一个整型变量,表示更新操作影响的行数。如果获取影响的行数的过程中出现错误,则会触发 panic。最后,使用 fmt.Printf() 函数输出更新操作影响的行数。
使用 limit 实现基本的分页查询
func main() {
// 打开数据库连接
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/test")
if err != nil {
panic(err.Error())
}
defer db.Close()
// 查询分页数据
pageSize := 20
pageNum := 1
offset := (pageNum - 1) * pageSize
rows, err := db.Query("SELECT id, name FROM users LIMIT ?, ?", offset, pageSize)
if err != nil {
panic(err.Error())
}
defer rows.Close()
// 遍历查询结果
for rows.Next() {
var (
id int
name string
)
if err := rows.Scan(&id, &name); err != nil {
panic(err.Error())
}
fmt.Printf("ID: %d; Name: %s\n", id, name)
}
// 检查遍历结果时是否出现错误
if err := rows.Err(); err != nil {
panic(err.Error())
}
}
例子使用了 SQL 查询语句 SELECT id, name FROM users LIMIT ?, ? 查询 users 表中的分页数据。通过执行 db.Query() 函数,将查询结果保存在 *sql.Rows 类型的结果集中。其中,offset 和 pageSize 分别表示分页查询的起始偏移量和每页数据的数量。注意,这里的 LIMIT 子句中,第一个问号表示偏移量,第二个问号表示数据数量。如果查询过程中出现错误,则会触发 panic。
接着,使用 rows.Next() 函数遍历查询结果集,检索每一行数据,使用 rows.Scan() 函数将查询结果映射到变量中。如果遍历过程中出现错误,将会触发 panic。最后,如果需要,使用 rows.Err() 函数检查遍历结果时是否出现错误。
查询 select count 累计数
func main() {
// 打开数据库连接
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/test")
if err != nil {
panic(err.Error())
}
defer db.Close()
// 查询记录数
var count int
err = db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
if err != nil {
panic(err.Error())
}
fmt.Printf("Number of records in users table: %d", count)
}
在上面的代码中,我们使用 SELECT COUNT(*) 查询获取了 users 表中的记录数量。使用 Scan() 函数将查询结果映射到 count 整型变量上。需要注意的是,必须传递指向 count 变量的指针作为参数,以便 Scan() 函数可以正确地将查询结果映射到变量中。
需要注意的是,在使用 Scan() 函数时,需要保证查询结果中字段的数量和类型与参数列表中的变量数量和类型完全匹配,否则会出现映射失败的情况。另外,需要根据实际情况检查 SQL 查询语句中使用的参数是否正确,以及获取数据时是否出现错误,并对错误进行处理。
使用 sqlx 直接 scan 到结构体
type User struct {
ID int `db:"id"`
Name string `db:"name"`
}
func main() {
// 打开数据库连接
db, err := sqlx.Open("mysql", "user:password@tcp(localhost:3306)/test")
if err != nil {
panic(err.Error())
}
defer db.Close()
// 查询一条记录
var user User
err = db.Get(&user, "SELECT id, name FROM users WHERE id = ?", 1)
if err != nil {
if err == sql.ErrNoRows {
// 没有符合条件的记录
return
}
panic(err.Error())
}
fmt.Printf("ID: %d; Name: %s", user.ID, user.Name)
}
在上面的代码中,我们使用了 SQLx 包将查询结果映射到了 User 结构体中。需要注意的是,这里使用了 db:"" 的标记来映射查询结果的列名与结构体成员。db:"id" 标记表示将 id 列映射到结构体的 ID 成员上,db:"name" 标记表示将 name 列映射到结构体的 Name 成员上。
换成 Select 函数就支持扫描多行
type User struct {
ID int `db:"id"`
Name string `db:"name"`
}
func main() {
// 打开数据库连接
db, err := sqlx.Open("mysql", "user:password@tcp(localhost:3306)/test")
if err != nil {
panic(err.Error())
}
defer db.Close()
// 查询多条记录
var users []User
err = db.Select(&users, "SELECT id, name FROM users WHERE age > ?", 18)
if err != nil {
panic(err.Error())
}
for _, user := range users {
fmt.Printf("ID: %d; Name: %s", user.ID, user.Name)
}
}
在上面的示例代码中,我们定义了一个结构体 User,并定义了一个结构体 Slice users。使用 SQLx 的 db.Select() 函数将查询结果映射到 users Slice 中。
得到记录后自行调用 StructScan
type User struct {
ID int `db:"id"`
Name string `db:"name"`
}
func main() {
// 打开数据库连接
db, err := sqlx.Open("mysql", "user:password@tcp(localhost:3306)/test")
if err != nil {
panic(err.Error())
}
defer db.Close()
// 查询多行记录,并将结果映射到 Rows 对象上
rows, err := db.Queryx("SELECT id, name FROM users WHERE age > ?", 18)
if err != nil {
panic(err.Error())
}
defer rows.Close()
// 遍历每一行记录,并将每一行记录映射到结构体上
var users []User
for rows.Next() {
var user User
if err := rows.StructScan(&user); err != nil {
panic(err)
}
users = append(users, user)
}
for _, user := range users {
fmt.Printf("ID: %d; Name: %s", user.ID, user.Name)
}
}
程序中通过 sqlx.Open() 函数连接到了 MySQL 数据库,并在执行完毕后通过 defer db.Close() 关闭了数据库连接,以避免造成资源浪费。
然后,我们使用 db.Queryx() 函数执行一个 SQL 查询语句,并将查询结果映射到一个 Rows 对象上。这个 SQL 查询语句使用了 WHERE 子句来过滤符合条件的记录,查询年龄大于 18 岁的用户的 id 和 name 字段信息。
在查询结果映射到 Rows 对象之后,我们使用 rows.Next() 函数遍历每一行结果,并使用 rows.StructScan() 函数将每一行结果映射到 User 结构体上。在映射完毕时,我们使用 fmt.Printf() 函数将 User 结构体中的字段数据输出到控制台上。需要注意的是,在使用 StructScan() 函数时,必须传递指向结构体的指针作为参数。
类似的,和 Get 对应的函数就是 db.QueryRowx 函数。
使用 sqlx 的命名参数和结构体
type User struct {
Name string `db:"name"`
Age int `db:"age"`
}
func main() {
// Open a database connection
db, err := sqlx.Open("mysql", "user:password@tcp(localhost:3306)/test")
if err != nil {
panic(err.Error())
}
defer db.Close()
// Insert a new row into the users table using a struct and named parameters
user := User{Name: "Bob", Age: 30}
_, err = db.NamedExec("INSERT INTO users (name, age) VALUES (:name, :age)", user)
if err != nil {
panic(err.Error())
}
}
在上面的代码中,我们使用了 NamedExec() 函数向 users 表中插入一条记录。使用结构体 User 来组织参数,并通过 :name 和 :age 命名参数显式地指定每个参数的名称和值,这样可以使 SQL 语句更具可读性和可维护性。
需要注意的是,使用 NamedExec() 函数时必须使用 :name 或 :name 形式的命名参数来标识每个参数,而不是使用 ? 占位符。
另外需要注意的是,虽然 NamedExec() 函数在使用命名参数时更容易使用和维护,但是在处理大量数据时,它可能会比 db.Exec() 函数慢一些,因为它需要额外的工作来处理命名参数。
使用 NamedQuery 来使用命名查询
type User struct {
Name string `db:"name"`
Age int `db:"age"`
}
func main() {
// 打开数据库连接
db, err := sqlx.Open("mysql", "user:password@tcp(localhost:3306)/test")
if err != nil {
panic(err.Error())
}
defer db.Close()
// 使用命名参数构造 SQL 语句
query, args, err := sqlx.Named("SELECT name, age FROM users WHERE age > :age", map[string]interface{}{
"age": 18,
})
if err != nil {
panic(err.Error())
}
// 执行 SQL 查询,并获得一个 Rows 对象
rows, err := db.NamedQuery(query, args)
if err != nil {
panic(err.Error())
}
defer rows.Close()
// 遍历每一行记录,并将其映射到结构体
for rows.Next() {
var user User
if err := rows.StructScan(&user); err != nil {
panic(err)
}
fmt.Printf("Name: %s; Age: %d", user.Name, user.Age)
}
}
NamedExec() 函数用于执行带有命名参数的 SQL 语句,并返回一个 sql.Result 对象,表示执行结果。
NamedQuery() 函数用于查询带有命名参数的 SQL 语句,并返回一个 *sqlx.Rows 对象,表示查询结果。它可以让我们在 SQL 语句中使用命名参数,从而使代码更加可读和易于维护。
简言之,NamedQuery() 函数用于查询带有命名参数的 SQL 语句并返回结果,而 NamedExec() 函数用于执行带有命名参数的 SQL 语句并返回执行结果。
使用标准库的预编译 SQL 提高效率
type User struct {
ID int `db:"id"`
Name string `db:"name"`
}
func main() {
// 打开数据库连接
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/database")
if err != nil {
panic(err.Error())
}
defer db.Close()
// 创建一个预编译语句
stmt, err := db.Prepare("SELECT * FROM users WHERE age > ?")
if err != nil {
panic(err.Error())
}
defer stmt.Close()
// 查询并遍历结果
rows, err := stmt.Query(18)
if err != nil {
panic(err.Error())
}
defer rows.Close()
var users []User
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Name); err != nil {
panic(err.Error())
}
users = append(users, user)
}
for _, user := range users {
fmt.Printf("ID: %d; Name: %s\n", user.ID, user.Name)
}
}
在这个示例中,我们首先打开了一个 MySQL 数据库连接。然后,我们使用 db.Prepare() 函数创建一个预编译语句,该语句可以在后续查询操作中重复使用。接下来,我们使用 stmt.Query() 函数执行预编译语句,并将参数绑定到 ? 占位符上。最后,我们遍历结果集,将每行记录映射到一个 User 结构体上。
注意,预编译语句的创建过程和使用过程与直接执行 SQL 语句的过程类似。不同之处在于,预编译语句可以在后续查询操作中重复使用,从而提高了查询效率。
sqlx 提供了命名的预编译语句
type User struct {
ID int `db:"id"`
Name string `db:"name"`
}
func main() {
// 打开数据库连接
db, err := sqlx.Open("mysql", "user:password@tcp(localhost:3306)/test")
if err != nil {
panic(err.Error())
}
defer db.Close()
// 创建一个命名参数的预编译语句
stmt, err := db.PrepareNamed("SELECT * FROM users WHERE age > :age")
if err != nil {
panic(err.Error())
}
defer stmt.Close()
// 查询并遍历结果
users := []User{}
if err := stmt.Select(&users, map[string]interface{}{
"age": 18,
}); err != nil {
panic(err.Error())
}
for _, user := range users {
fmt.Printf("ID: %d; Name: %s\n", user.ID, user.Name)
}
}
在这个示例中,我们使用 db.PrepareNamed() 函数创建了一个命名参数的预编译语句,并使用 stmt.Select() 函数执行了一个查询操作,其中 map[string]interface{} 参数包含了命名参数和对应的值。Select() 函数可以将查询结果映射到一个结构体 Slice 上,从而使代码更加简洁和易读。
sqlx 对预编译语句提供了一些新的功能以提高查询效率和易用性。在 sqlx 中,你可以使用 NamedStmt 结构体来表示一个预编译语句,并使用 PrepareNamed() 函数来创建一个 NamedStmt 对象。然后,你可以使用命名参数执行 SQL 查询,从而使代码更加可读和易于维护。
类似的,Preparex() 的作用与标准库的 Prepare() 函数类似。它们都可以用来创建预编译语句,以提高查询效率和安全性。不同之处在于,Preparex() 函数是 sqlx 包提供的,它可以与 sqlx 的其他函数和类型一起使用,比如 NamedExec() 和 StructScan() 等函数。Preparex() 函数还支持命名参数和结构体等特性,可以让代码更加简洁和易读。
ORM 框架 gorm 单表查询
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"column:name"`
Age uint `gorm:"column:age"`
}
func main() {
// 连接 MySQL 数据库
dsn := "user:password@tcp(localhost:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(err)
}
// 自动迁移 User 模型
db.AutoMigrate(&User{})
// 创建一个用户
user := User{Name: "Bob", Age: 30}
result := db.Create(&user)
fmt.Println(result.RowsAffected)
// 查询所有用户
users := []User{}
db.Find(&users)
fmt.Println(users)
}
在这个示例中,我们首先使用 gorm.Open() 函数连接了一个 MySQL 数据库。然后,我们使用 AutoMigrate() 函数进行模型迁移,以便让 gorm 知道如何将模型映射成数据库表。
接下来,我们使用 Create() 方法创建了一个用户,并使用 Find() 方法查询了所有用户。在这些操作中,我们都使用了 gorm 的链式 API,这使得代码更加简洁和易读。
使用 gorm 实体关系 has_many 查询
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"column:name"`
Age uint `gorm:"column:age"`
Books []Book // has_many 关系
}
type Book struct {
ID uint `gorm:"primaryKey"`
Title string `gorm:"column:title"`
Author string `gorm:"column:author"`
UserID uint `gorm:"column:user_id"`
}
func main() {
// 连接 MySQL 数据库
dsn := "user:password@tcp(localhost:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(err)
}
// 自动迁移 User 和 Book 模型
db.AutoMigrate(&User{}, &Book{})
// 创建用户和图书
user := User{Name: "Bob", Age: 30}
book1 := Book{Title: "The Great Gatsby", Author: "F. Scott Fitzgerald"}
book2 := Book{Title: "To Kill a Mockingbird", Author: "Harper Lee"}
db.Create(&user)
db.Model(&user).Association("Books").Append([]Book{book1, book2})
// 查询用户及其所有图书
var result struct {
User
Books []Book
}
db.Preload("Books").First(&result, "name = ?", "Bob")
fmt.Println(result)
}
在这个示例中,我们定义了两个模型:User 和 Book,它们之间存在一对多的关系(一个用户可以有多本书)。在 User 模型中,我们使用 Books 字段定义了一个 slice,用于存储用户所拥有的图书。在 Book 模型中,我们使用 UserID 字段表示该图书所属的用户 ID。
接下来,我们使用 Association() 和 Append() 方法来将两本书关联到用户对象上。然后,我们使用 Preload() 方法将用户及其所有图书一起查询出来,并将结果存储在一个名为 result 的结构体中。
需要特别注意的是,在查询过程中,我们使用了 Preload() 方法来预加载用户所拥有的图书,从而避免了多次查询数据库,提高了查询效率和性能。
总之,gorm 支持多种实体关系查询,使得我们可以更加便捷和灵活地操作数据库,并且不需要编写冗长的 SQL 语句。
使用 gorm 实体关系 belongs_to 查询
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"column:name"`
Age uint `gorm:"column:age"`
}
type Book struct {
ID uint `gorm:"primaryKey"`
Title string `gorm:"column:title"`
Author string `gorm:"column:author"`
UserID uint `gorm:"column:user_id"`
User User `gorm:"foreignKey:UserID"`
}
func main() {
// 连接 MySQL 数据库
dsn := "user:password@tcp(localhost:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(err)
}
// 自动迁移 User 和 Book 模型
db.AutoMigrate(&User{}, &Book{})
// 创建用户和图书
user := User{Name: "Bob", Age: 30}
book1 := Book{Title: "The Great Gatsby", Author: "F. Scott Fitzgerald", User: user}
book2 := Book{Title: "To Kill a Mockingbird", Author: "Harper Lee", User: user}
db.Create(&user)
db.Create(&book1)
db.Create(&book2)
// 查询所有图书及其所属用户
var books []Book
if err := db.Preload("User").Find(&books).Error; err != nil {
panic(err)
}
for _, book := range books {
fmt.Printf("Book: %s, Author: %s, User: %s\n", book.Title, book.Author, book.User.Name)
}
}
在这个示例中,我们定义了 User 和 Book 两个模型。我们删除了 User 模型中的 Books 字段,只在 Book 模型中使用了 BelongsTo 关键字来表示该图书属于哪个用户。在查询过程中,我们使用 Preload() 方法预加载了每个图书所属的用户,并将结果存储在一个名为 books 的结构体切片中。
需要注意的是,在查询语句中,我们使用了 Preload("User") 来预加载每个图书所属的用户,这样查询结果中就包含了 User 模型的信息。