通過實例理解 Go 訪問和操作數據庫的幾種方式

關係數據庫操作是 Go 應用開發中的重要一環,尤其是 Go Web 應用、微服務等。作爲 Gopher,我們需要了解幾種主流的數據庫訪問和操作方法,以便在項目中做出適當的選擇。

我個人在日常開發中較少涉及 CRUD 類應用,因此使用 Go 訪問和操作數據庫的機會並不多,在這方面也算是有一些 “短板”。通過在這篇文章中對數據庫訪問方式進行全面的梳理,我也算是補全一下技能樹,同時也能爲讀者小夥伴提供一些參考。

我搜集了目前 Go 社區的主流數據庫訪問和操作方式,大致有如下幾種:

在這篇文章中,我會建立一個簡單的關係數據庫實例,並用一個簡單的學校院系選課關係模型作爲示例,分別用上述幾種方法實現數據庫訪問以及 CRUD 操作,並對比各種方式的操作性能。通過對比,你可以瞭解每種方法的特點。希望這些例子能幫助各位讀者在實際項目中更好地處理數據庫操作。

  1. 建立示例數據庫和數據庫模式 (schema)

爲了便於後續代碼示例的講解和實現,我們先來建立示例數據庫並定義數據庫模式。

1.1 基於容器啓動 MySQL 數據庫服務

在本文中,我們選擇關係數據庫界的主流代表 MySQL 數據庫 [5]。基於容器,我們可以很方便地啓動 MySQL 數據庫服務:

$docker pull mysql:latest
$docker run -d --name mysql-db -v /path/to/host/mysqldata:/var/lib/mysql -p 4407:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:latest

由於做了 volume 掛載,MySQL 容器內部的數據文件將會存儲在主機的 / path/to/host/mysqldata 目錄下,即使容器被刪除或重新創建,數據文件也不會丟失。你可以根據實際情況替換 / path/to/host/mysqldata 爲你想要存儲 MySQL 數據的主機目錄路徑。

如果容器啓動成功,我們可以通過容器內的 mysql client 工具連接到 MySQL 數據庫中:

$docker exec -it mysql-db mysql -uroot -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.2.0 MySQL Community Server - GPL

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

我們在 MySQL 中創建 example_db 數據庫供後面的數據庫建表和數據操作使用:

mysql> CREATE DATABASE example_db;
Query OK, 1 row affected (0.01 sec)

mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| example_db         |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.01 sec)

1.2 建立數據庫模式

接下來,我將借用並簡化《Database System Concepts,7th》[6] 一書中提供的示例數據庫的 Schema,創建本文後續代碼示例使用的數據庫表。簡化後的 Schema 將涵蓋常見的 CRUD 操作需求,同時保證數據庫結構清晰易懂。下面是簡化後的數據模式對應的 E-R 圖 (基於在線 https://dbdiagram.io / 工具生成,dbml 源文件在 database-access/schema.dbml):

這個 Schema 包括 department(院系表)、instructor(教師表)、course(課程信息表)、student(學生信息表) 和 enrollment(學生選課信息)。下面是建表語句:

// database-access/schema.sql
DROP DATABASE IF EXISTS example_db;
CREATE DATABASE example_db;

CREATE TABLE department (
    dept_id INT AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    PRIMARY KEY (dept_id)
);

CREATE TABLE instructor (
    instr_id INT AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    dept_id INT,
    PRIMARY KEY (instr_id),
    FOREIGN KEY (dept_id) REFERENCES department(dept_id)
);

CREATE TABLE course (
    course_id INT AUTO_INCREMENT,
    title VARCHAR(100) NOT NULL,
    dept_id INT,
    PRIMARY KEY (course_id),
    FOREIGN KEY (dept_id) REFERENCES department(dept_id)
);

CREATE TABLE student (
    student_id INT AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    dept_id INT,
    PRIMARY KEY (student_id),
    FOREIGN KEY (dept_id) REFERENCES department(dept_id)
);

CREATE TABLE enrollment (
    student_id INT,
    course_id INT,
    semester VARCHAR(6),
    year INT,
    PRIMARY KEY (student_id, course_id, semester, year),
    FOREIGN KEY (student_id) REFERENCES student(student_id),
    FOREIGN KEY (course_id) REFERENCES course(course_id)
);

通過 mysql client 工具執行上述語句後,我們就完成了表的創建:

mysql> show tables;
+----------------------+
| Tables_in_example_db |
+----------------------+
| course               |
| department           |
| enrollment           |
| instructor           |
| student              |
+----------------------+
5 rows in set (0.00 sec)

不過在開始使用 Go 語言來訪問並操作這些數據表之前,我們先定義一些基本的數據庫表操作的示例,後續每種 Go 訪問和操作數據庫的方式都會基於這些示例並實現這些示例中的操作。

  1. 定義數據庫表操作示例

2.1 插入數據(Create)

向 department 表中插入數據:

INSERT INTO department (name) VALUES ('Computer Science');
INSERT INTO department (name) VALUES ('Mathematics');

向 instructor 表中插入數據:

INSERT INTO instructor (name, dept_id) VALUES ('John Doe', 1);
INSERT INTO instructor (name, dept_id) VALUES ('Jane Smith', 2);

向 course 表中插入數據:

INSERT INTO course (title, dept_id) VALUES ('Database Systems', 1);
INSERT INTO course (title, dept_id) VALUES ('Calculus', 2);

向 student 表中插入數據:

INSERT INTO student (name, dept_id) VALUES ('Alice', 1);
INSERT INTO student (name, dept_id) VALUES ('Bob', 2);

向 enrollment 表中插入數據:

INSERT INTO enrollment (student_id, course_id, semester, year) VALUES (1, 1, 'Fall', 2024);
INSERT INTO enrollment (student_id, course_id, semester, year) VALUES (2, 2, 'Fall', 2024);

2.2 查詢數據(Retrieve)

查詢所有學生的信息:

SELECT * FROM student;

查詢某個院系的課程信息:

SELECT * FROM course WHERE dept_id = 1;

查詢某個學生的選課信息:

SELECT * FROM enrollment WHERE student_id = 1;

2.3 更新數據(Update)

更新某個學生的姓名:

UPDATE student SET name = 'Alice Johnson' WHERE student_id = 1;

更新某個課程的標題:

UPDATE course SET title = 'Advanced Database Systems' WHERE course_id = 1;

2.4 刪除數據(Delete)

刪除某個學生的選課記錄:

DELETE FROM enrollment WHERE student_id = 1 AND course_id = 1 AND semester = 'Fall' AND year = 2024;

刪除某個課程:

DELETE FROM course WHERE course_id = 1;

通過上述定義的這些示例操作,我們可以對數據庫進行基本的增刪改查操作。接下來,我們就來正式介紹 Go 訪問和操作數據庫的幾種主流方式,並分別用這些方式來實現上述示例的 CRUD 操作。

我們先來看一下基於 Go 標準庫的數據庫訪問和操作方式。

  1. 採用 Go 標準庫的數據庫訪問方式

Go 標準庫中提供了一個 database/sql 包,它定義了一些接口和方法,用於訪問關係數據庫。這個包提供了一個抽象層,可以與各種不同的關係數據庫驅動程序進行交互。比如 database/sql 包定義了一些接口,如 DB、Conn、Stmt 等,用於表示數據庫連接、語句執行等操作。數據庫驅動包需要實現這些接口,並提供了具體的數據庫交互邏輯。

Go 應用使用 database/sql 包時,應用首先需要導入數據庫驅動程序,然後使用 sql.Open 函數連接到數據庫。這個函數返回一個 * sql.DB 對象,代表數據庫連接。之後,Go 應用便可以使用 DB 對象執行各種 SQL 操作, 如 DB.Query、DB.Exec 等。這些函數會調用驅動程序中實現的具體方法來與數據庫交互。 對於對於複雜的數據庫查詢操作,Go 應用還可以使用 DB 對象創建 * sql.Stmt 對象,後者表示預編譯好的 SQL 語句,這樣可以提高操作性能。

總的來說,database/sql 包提供了一個標準化的接口,讓應用程序可以方便地訪問不同的關係數據庫,而不需要關心底層的實現細節。這使得 Go 程序可以跨數據庫平臺運行。

下面我們就基於 go-sql-driver/mysql[7] 提供的 MySQL 驅動來實現對 MySQL 中示例表的各種操作。

3.1 初始化數據庫連接

我們首先需要在代碼中初始化數據庫連接。以下是初始化代碼示例:

// database-access/stdlib/main.go
package main

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql" // 註冊mysql driver
    "log"
)

func main() {
    dsn := "root:123456@tcp(127.0.0.1:4407)/example_db"
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // 測試數據庫連接
    if err := db.Ping(); err != nil {
        log.Fatal(err)
    }
    fmt.Println("Connected to the database successfully!")
}

拿到數據庫實例 (*sql.DB 對象) 後,我們便可以基於該實例對數據庫表進行各種操作了!接下來,我們逐一看一下。

3.2 插入數據(Create)

以下是通過 Go 標準庫 database/sql 包方式插入數據的代碼示例:

func insertData(db *sql.DB) {
    // 插入department數據
    _, err := db.Exec("INSERT INTO department (name) VALUES ('Computer Science'), ('Mathematics')")
    if err != nil {
        log.Fatal(err)
    }

    // 插入instructor數據
    _, err = db.Exec("INSERT INTO instructor (name, dept_id) VALUES ('John Doe', 1), ('Jane Smith', 2)")
    if err != nil {
        log.Fatal(err)
    }

    // 插入course數據
    _, err = db.Exec("INSERT INTO course (title, dept_id) VALUES ('Database Systems', 1), ('Calculus', 2)")
    if err != nil {
        log.Fatal(err)
    }

    // 插入student數據
    _, err = db.Exec("INSERT INTO student (name, dept_id) VALUES ('Alice', 1), ('Bob', 2)")
    if err != nil {
        log.Fatal(err)
    }

    // 插入enrollment數據
    _, err = db.Exec("INSERT INTO enrollment (student_id, course_id, semester, year) VALUES (1, 1, 'Fall', 2024), (2, 2, 'Fall', 2024)")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Data inserted successfully!")
}

3.3 查詢數據(Retrieve)

以下是查詢數據的代碼示例:

func queryData(db *sql.DB) {
    // 查詢所有學生的信息
    rows, err := db.Query("SELECT * FROM student")
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
        var studentID int
        var name string
        var deptID int
        err := rows.Scan(&studentID, &name, &deptID)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("Student ID: %d, Name: %s, Department ID: %d\n", studentID, name, deptID)
    }

    // 查詢某個院系的課程信息
    rows, err = db.Query("SELECT * FROM course WHERE dept_id = ?", 1)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
        var courseID int
        var title string
        var deptID int
        err := rows.Scan(&courseID, &title, &deptID)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("Course ID: %d, Title: %s, Department ID: %d\n", courseID, title, deptID)
    }

    // 查詢某個學生的選課信息
    rows, err = db.Query("SELECT * FROM enrollment WHERE student_id = ?", 1)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
        var studentID int
        var courseID int
        var semester string
        var year int
        err := rows.Scan(&studentID, &courseID, &semester, &year)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("Student ID: %d, Course ID: %d, Semester: %s, Year: %d\n", studentID, courseID, semester, year)
    }
}

3.4 更新數據(Update)

以下是更新數據的代碼示例:

func updateData(db *sql.DB) {
    // 更新某個學生的姓名
    _, err := db.Exec("UPDATE student SET name = 'Alice Johnson' WHERE student_id = ?", 1)
    if err != nil {
        log.Fatal(err)
    }

    // 更新某個課程的標題
    _, err = db.Exec("UPDATE course SET title = 'Advanced Database Systems' WHERE course_id = ?", 1)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Data updated successfully!")
}

3.5 刪除數據(Delete)

以下是刪除數據的代碼示例:

func deleteData(db *sql.DB) {
    // 刪除某個學生的選課記錄
    _, err := db.Exec("DELETE FROM enrollment WHERE student_id = ? AND course_id = ? AND semester = ? AND year = ?", 1, 1, "Fall", 2024)
    if err != nil {
        log.Fatal(err)
    }

    // 刪除某個課程
    _, err = db.Exec("DELETE FROM course WHERE course_id = ?", 1)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Data deleted successfully!")
}

注:上述示例的完整代碼可以參見 database-access/stdlib/main.go。

通過上述代碼示例,我們展示瞭如何使用 Go 標準庫和 MySQL 驅動程序來進行數據庫連接和基本的 CRUD 操作。我們看到直接使用 Go 標準庫的 database/sql 包來訪問和操作數據庫確實是比較基礎和原始的方式,基本上是手動拼接 SQL 語句和處理結果,這種方式確實比較低級和繁瑣。

sqlx 包在一定程度上提升了 Go 標準庫訪問數據庫的體驗,並完全兼容 database/sql 包的接口,接下來,我們就來看看如何使用 database/sql 的擴展庫 sqlx 來訪問和操作數據庫。

3.6 使用 sqlx 擴展庫訪問 MySQL 數據庫

sqlx[8] 是一個擴展庫,它在 Go 的標準 database/sql 庫之上提供了一系列擴展。sqlx 版本的 sql.DB、sql.TX、sql.Stmt 等所有接口都保留了底層接口不變,這意味着它們的接口是標準庫接口的超集,這使得我們可以無縫地將現有使用 database/sql 的代碼集成到 sqlx 中。sqlx 的主要擴展功能包括:

sqlx 在保持 database/sql 接口不變的情況下,提供了許多額外的功能和便利性,使得在 Go 中訪問關係型數據庫變得更加簡單高效。下面是使用 sqlx 實現的上面示例操作的完整代碼:

// database-access/sqlx/main.go

package main

import (
 "fmt"
 "log"

 _ "github.com/go-sql-driver/mysql"
 "github.com/jmoiron/sqlx"
)

func main() {
 dsn := "root:123456@tcp(127.0.0.1:4407)/example_db"
 db, err := sqlx.Connect("mysql", dsn)
 if err != nil {
  log.Fatal(err)
 }
 defer db.Close()

 fmt.Println("Connected to the database successfully!")

 insertData(db)
 queryData(db)
 updateData(db)
 queryData(db) // 查看更新後的數據
 deleteData(db)
 queryData(db) // 查看刪除後的數據
}

func insertData(db *sqlx.DB) {
 // 插入department數據
 _, err := db.NamedExec(`INSERT INTO department (name) VALUES (:name)`[]map[string]interface{}{
  {"name""Computer Science"},
  {"name""Mathematics"},
 })
 if err != nil {
  log.Fatal(err)
 }

 // 插入instructor數據
 _, err = db.NamedExec(`INSERT INTO instructor (name, dept_id) VALUES (:name, :dept_id)`[]map[string]interface{}{
  {"name""John Doe""dept_id": 1},
  {"name""Jane Smith""dept_id": 2},
 })
 if err != nil {
  log.Fatal(err)
 }

 // 插入course數據
 _, err = db.NamedExec(`INSERT INTO course (title, dept_id) VALUES (:title, :dept_id)`[]map[string]interface{}{
  {"title""Database Systems""dept_id": 1},
  {"title""Calculus""dept_id": 2},
 })
 if err != nil {
  log.Fatal(err)
 }

 // 插入student數據
 _, err = db.NamedExec(`INSERT INTO student (name, dept_id) VALUES (:name, :dept_id)`[]map[string]interface{}{
  {"name""Alice""dept_id": 1},
  {"name""Bob""dept_id": 2},
 })
 if err != nil {
  log.Fatal(err)
 }

 // 插入enrollment數據
 _, err = db.NamedExec(`INSERT INTO enrollment (student_id, course_id, semester, year) VALUES (:student_id, :course_id, :semester, :year)`[]map[string]interface{}{
  {"student_id": 1, "course_id": 1, "semester""Fall""year": 2024},
  {"student_id": 2, "course_id": 2, "semester""Fall""year": 2024},
 })
 if err != nil {
  log.Fatal(err)
 }

 fmt.Println("Data inserted successfully!")
}

type Student struct {
 StudentID int    `db:"student_id"`
 Name      string `db:"name"`
 DeptID    int    `db:"dept_id"`
}

type Course struct {
 CourseID int    `db:"course_id"`
 Title    string `db:"title"`
 DeptID   int    `db:"dept_id"`
}

type Enrollment struct {
 StudentID int    `db:"student_id"`
 CourseID  int    `db:"course_id"`
 Semester  string `db:"semester"`
 Year      int    `db:"year"`
}

func queryData(db *sqlx.DB) {
 // 查詢所有學生的信息
 var students []Student
 err := db.Select(&students, "SELECT * FROM student")
 if err != nil {
  log.Fatal(err)
 }
 for _, student := range students {
  fmt.Printf("Student ID: %d, Name: %s, Department ID: %d\n", student.StudentID, student.Name, student.DeptID)
 }

 // 查詢某個院系的課程信息
 var courses []Course
 err = db.Select(&courses, "SELECT * FROM course WHERE dept_id = ?", 1)
 if err != nil {
  log.Fatal(err)
 }
 for _, course := range courses {
  fmt.Printf("Course ID: %d, Title: %s, Department ID: %d\n", course.CourseID, course.Title, course.DeptID)
 }

 // 查詢某個學生的選課信息
 var enrollments []Enrollment
 err = db.Select(&enrollments, "SELECT * FROM enrollment WHERE student_id = ?", 1)
 if err != nil {
  log.Fatal(err)
 }
 for _, enrollment := range enrollments {
  fmt.Printf("Student ID: %d, Course ID: %d, Semester: %s, Year: %d\n", enrollment.StudentID, enrollment.CourseID, enrollment.Semester, enrollment.Year)
 }
}

func updateData(db *sqlx.DB) {
 // 更新某個學生的姓名
 _, err := db.NamedExec("UPDATE student SET name = :name WHERE student_id = :student_id", map[string]interface{}{
  "name":       "Alice Johnson",
  "student_id": 1,
 })
 if err != nil {
  log.Fatal(err)
 }

 // 更新某個課程的標題
 _, err = db.NamedExec("UPDATE course SET title = :title WHERE course_id = :course_id", map[string]interface{}{
  "title":     "Advanced Database Systems",
  "course_id": 1,
 })
 if err != nil {
  log.Fatal(err)
 }

 fmt.Println("Data updated successfully!")
}

func deleteData(db *sqlx.DB) {
 // 刪除某個學生的選課記錄
 _, err := db.NamedExec("DELETE FROM enrollment WHERE student_id = :student_id AND course_id = :course_id AND semester = :semester AND year = :year", map[string]interface{}{
  "student_id": 1,
  "course_id":  1,
  "semester":   "Fall",
  "year":       2024,
 })
 if err != nil {
  log.Fatal(err)
 }

 // 刪除某個課程
 _, err = db.NamedExec("DELETE FROM course WHERE course_id = :course_id", map[string]interface{}{
  "course_id": 1,
 })
 if err != nil {
  log.Fatal(err)
 }

 fmt.Println("Data deleted successfully!")
}

我們看到:相較於直接使用 database/sql,sqlx 的 named query/exec 和直接將結果寫入結構體 / map/slices 的確非常方便! 代碼也顯得更加簡潔、易讀。

不過要說方便和易讀,對象關係映射 (ORM) 方式說自己第二,沒人敢說是第一。下面我們就來看看在 Go 中訪問和操作數據庫最常使用的方式:ORM 方式。

  1. 使用 ORM 庫訪問數據庫

ORM(Object-Relational Mapping)是一種通過對象方式來操作數據庫的方法,它將數據庫中的表映射爲程序中的對象,使開發者可以使用面向對象的方式操作數據庫。使用 ORM 庫可以簡化數據庫操作,提高開發效率,同時也能減少手寫 SQL 帶來的錯誤風險。

Go 社區有幾個很受歡迎的 ORM 庫,比如 gorm、xorm[9] 等。接下來我將以最常用的 Go ORM 庫 GORM 來說明一下如何使用 ORM 訪問和操作數據庫。

GORM 是一個功能強大的 Go ORM 庫,它提供了豐富的特性,如自動遷移 (migrate)、關聯、鉤子、事務、複合主鍵等。GORM 支持多種數據庫,包括 MySQL、PostgreSQL、SQLite 等。

和採用原生 database/sql 的方式不同,使用 ORM 方式訪問數據庫,我們首先先要定義表對應的對象,即創建對象模型。

4.1 創建對象模型

下面的各個結構體類型對應的就是示例中各個表,gorm 通過 struct field tag 來將結構體字段與表的列字段對應在一起:

// database-access/gorm/main.go

type Department struct {
 ID   uint   `gorm:"primaryKey"`
 Name string `gorm:"size:100;not null"`
}

type Instructor struct {
 ID     uint   `gorm:"primaryKey"`
 Name   string `gorm:"size:100;not null"`
 DeptID uint
 Dept   Department `gorm:"foreignKey:DeptID"`
}

type Course struct {
 ID     uint   `gorm:"primaryKey"`
 Title  string `gorm:"size:100;not null"`
 DeptID uint
 Dept   Department `gorm:"foreignKey:DeptID"`
}

type Student struct {
 ID     uint   `gorm:"primaryKey"`
 Name   string `gorm:"size:100;not null"`
 DeptID uint
 Dept   Department `gorm:"foreignKey:DeptID"`
}

type Enrollment struct {
 ID        uint `gorm:"primaryKey"`
 StudentID uint
 CourseID  uint
 Semester  string  `gorm:"size:50;not null"`
 Year      int     `gorm:"not null"`
 Student   Student `gorm:"foreignKey:StudentID"`
 Course    Course  `gorm:"foreignKey:CourseID"`
 CreatedAt time.Time
 UpdatedAt time.Time
}

4.2 CRUD 操作示例

下面就是基於上面定義的 ORM 模型進行 CRUD 操作的示例代碼:

// database-access/gorm/main.go
func main() {
 dsn := "root:123456@tcp(127.0.0.1:4407)/example_db?charset=utf8mb4&parseTime=True&loc=Local"
 db, err := gorm.Open(mysql.Open(dsn)&gorm.Config{
  NamingStrategy: schema.NamingStrategy{
   SingularTable: true,
  },
 })
 if err != nil {
  log.Fatal(err)
 }

 // 自動遷移模式
 db.AutoMigrate(&Department{}&Instructor{}&Course{}&Student{}&Enrollment{})

 // 執行CRUD操作
 createData(db)
 queryData(db)
 updateData(db)
 deleteData(db)
}

func createData(db *gorm.DB) {
 // 創建院系
 cs := Department{Name: "Computer Science"}
 math := Department{Name: "Mathematics"}
 db.Create(&cs)
 db.Create(&math)

 // 創建教師
 db.Create(&Instructor{Name: "John Doe", DeptID: cs.ID})
 db.Create(&Instructor{Name: "Jane Smith", DeptID: math.ID})

 // 創建課程
 db.Create(&Course{Title: "Database Systems", DeptID: cs.ID})
 db.Create(&Course{Title: "Calculus", DeptID: math.ID})

 // 創建學生
 db.Create(&Student{Name: "Alice", DeptID: cs.ID})
 db.Create(&Student{Name: "Bob", DeptID: math.ID})

 // 學生選課
 db.Create(&Enrollment{StudentID: 1, CourseID: 1, Semester: "Fall", Year: 2024})
 db.Create(&Enrollment{StudentID: 2, CourseID: 2, Semester: "Fall", Year: 2024})
}

func queryData(db *gorm.DB) {
 // 查詢所有學生
 var students []Student
 db.Find(&students)
 for _, student := range students {
  log.Printf("Student ID: %d, Name: %s, Department ID: %d\n", student.ID, student.Name, student.DeptID)
 }

 // 查詢某個院系的課程
 var courses []Course
 db.Where("dept_id = ?", 1).Find(&courses)
 for _, course := range courses {
  log.Printf("Course ID: %d, Title: %s, Department ID: %d\n", course.ID, course.Title, course.DeptID)
 }

 // 查詢某個學生的選課信息
 var enrollments []Enrollment
 db.Where("student_id = ?", 1).Find(&enrollments)
 for _, enrollment := range enrollments {
  log.Printf("Student ID: %d, Course ID: %d, Semester: %s, Year: %d\n", enrollment.StudentID, enrollment.CourseID, enrollment.Semester, enrollment.Year)
 }
}

func updateData(db *gorm.DB) {
 // 更新學生姓名
 db.Model(&Student{}).Where("id = ?", 1).Update("name""Alice Johnson")

 // 更新課程標題
 db.Model(&Course{}).Where("id = ?", 1).Update("title""Advanced Database Systems")
}

func deleteData(db *gorm.DB) {
 // 刪除選課記錄
 db.Where("course_id = ?", 1).Delete(&Enrollment{})

 // 刪除課程
 db.Where("id = ?", 1).Delete(&Course{})
}

我們看到 GORM 提供了大量的便捷方法,可以大幅度簡化 SQL 操作。例如,插入記錄只需調用 Create 方法,而不需要手寫 SQL 語句。GORM 還提供了多種鉤子函數(如 BeforeCreate, AfterCreate, BeforeUpdate, AfterUpdate 等),可以在特定操作前後執行自定義邏輯。這對於實現複雜業務邏輯非常有幫助。示例裏沒有使用鉤子函數,大家可以自行試驗。

日常使用數據庫,查詢操作佔比最大,GORM 的查詢構造器允許開發人員使用鏈式方法調用來構造複雜的查詢條件,例如,我們可以使用 Where, Or, Order, Limit, Offset 等方法來構建查詢。

GORM 還提供了 AutoMigrate 方法,可以根據模型結構自動創建或更新數據庫表,這在開發環境中十分實用,減少了手動管理數據庫結構的複雜性。

此外,GORM 支持一對一、一對多和多對多等多種關聯關係,並且可以通過簡單的模型定義和方法調用來操作這些關係。就像例子中那樣,我們可以通過定義 foreignKey 來自動管理外鍵約束。

總之,ORM 方式的數據庫訪問和操作大幅降低了開發人員使用數據庫的複雜性,提高了生產效率。不過由於引入了一層新的抽象,在表數據量較大的情況下,ORM 方式的性能相對於原生 SQL 要低一些,這個我們在後面的對比各種方式的性能一節會再說。

Go 開發人員在使用數據庫時,往往希望能夠在以下幾個方面達到平衡:

Go 標準庫的 database/sql 包提供了直接操作 SQL 語句的方式,可以發揮底層數據庫引擎的性能優勢。相比之下,ORM 庫在一定程度上會增加性能開銷。

ORM 庫能夠提供更高級的抽象和自動化,簡化了許多數據庫操作的樣板代碼,使得開發體驗更加友好,生產力也相對較高。

ORM 庫通常能夠提供更好的類型安全性,減少手動拼接 SQL 語句時出錯的風險。

簡單來說,就是我們希望 “既要..,也要...,還要...”,於是便有了以代碼生成方式訪問和操作數據庫的代表 sqlc。接下來我們就來看看如何 sqlc 是如何用代碼生成方式來訪問和操作數據庫的。

  1. 使用代碼生成方式訪問數據庫

sqlc 是一個強大的工具,它可以將針對數據庫的操作,比如 SQL 查詢等,直接生成類型安全的 Go 代碼。它不僅保留了 SQL 的靈活性和可讀性,同時也提供了編譯時的類型檢查,可以避免手寫 SQL 代碼中的錯誤。

5.1 安裝 sqlc

和上面的兩種方式不同,使用 sqlc,我們需要首先安裝 sqlc cmdline 工具 [10],這個工具用來基於 sqlc 定義的一套 SQL dsl 語法 [11] 生成相應的 Go 代碼。

通過下面命令可以實現 sqlc 工具的安裝:

$go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest

安裝後,輸入下面命令驗證一下 sqlc 的安裝結果,如果輸出下面內容,則說明安裝 ok 了:

$sqlc -h
Usage:
  sqlc [command]

Available Commands:
  compile     Statically check SQL for syntax and type errors
  completion  Generate the autocompletion script for the specified shell
  createdb    Create an ephemeral database
  diff        Compare the generated files to the existing files
  generate    Generate source code from SQL
  help        Help about any command
  init        Create an empty sqlc.yaml settings file
  push        Push the schema, queries, and configuration for this project
  verify      Verify schema, queries, and configuration for this project
  version     Print the sqlc version number
  vet         Vet examines queries

Flags:
  -f, --file string   specify an alternate config file (default: sqlc.yaml)
  -h, --help          help for sqlc
      --no-remote     disable remote execution (default: false)
      --remote        enable remote execution (default: false)

Use "sqlc [command] --help" for more information about a command.

5.2 初始化和配置 sqlc 項目

下面是 sqlc 的代碼生成上的輸入與輸出示意圖:

我們看到要生成 Go 代碼,我們需要提供三個輸入文件,其中 sqlc.yaml 是 sqlc 項目的主配置文件,它是個 yaml 格式文件 [12],在我們這個示例中,它的內容如下:

// database-access/sqlc/sqlc.yaml
version: "2"
sql:
  - name: "db"
    engine: "mysql"
    queries: "queries.sql"
    schema: "schema.sql"
    gen:
      go:
        package: "db"
        out: "db"

這個文件可以使用 sqlc init 生成一個模板,然後再向其中填寫具體內容。上述 sqlc.yaml 的內容不難理解,其中 engine 表示生成的代碼將用於與 MySQL 交互,schema 是數據庫模式文件,queries.sql 中定義了與數據庫的所有交互語句,而在 gen 段中,package 是輸出的代碼的包名,而 out 則是輸出到哪個目錄下。

接下來,我們再來看看 schema.sql 和 queries.sql。

5.3 創建數據庫模式和查詢文件

schema.sql 文件的內容與我們

-- schema.sql

CREATE TABLE department (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE instructor (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    dept_id INT,
    FOREIGN KEY (dept_id) REFERENCES department(id)
);

CREATE TABLE course (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(100) NOT NULL,
    dept_id INT,
    FOREIGN KEY (dept_id) REFERENCES department(id)
);

CREATE TABLE student (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    dept_id INT,
    FOREIGN KEY (dept_id) REFERENCES department(id)
);

CREATE TABLE enrollment (
    student_id INT,
    course_id INT,
    semester VARCHAR(50) NOT NULL,
    year INT NOT NULL,
    PRIMARY KEY (student_id, course_id, semester, year),
    FOREIGN KEY (student_id) REFERENCES student(id),
    FOREIGN KEY (course_id) REFERENCES course(id)
);

沒錯,這就是一些建表語句,後續 sqlc 執行生成命令時會參考這些表以及約束。queries.sql 則是我們要使用的數據庫 dml 語句:

-- name: CreateDepartment :execresult
INSERT INTO department (
  name
) VALUES (
  ?
);

-- name: GetDepartments :many
SELECT id, name FROM department;

-- name: CreateInstructor :execresult
INSERT INTO instructor (
  name, dept_id
) VALUES (
  ?, ?
);

-- name: GetInstructors :many
SELECT id, name, dept_id FROM instructor;

-- name: CreateCourse :execresult
INSERT INTO course (
  title, dept_id
) VALUES (
  ?, ?
);

-- name: GetCoursesByDept :many
SELECT id, title, dept_id FROM course WHERE dept_id = ?;

-- name: CreateStudent :execresult
INSERT INTO student (
  name, dept_id
) VALUES (
  ?, ?
);

-- name: GetStudents :many
SELECT id, name, dept_id FROM student;

-- name: EnrollStudent :execresult
INSERT INTO enrollment (
  student_id, course_id, semester, year
) VALUES (
  ?, ?, ?, ?
);

-- name: GetEnrollmentByStudent :many
SELECT student_id, course_id, semester, year FROM enrollment WHERE student_id = ?;

-- name: UpdateStudentName :exec
UPDATE student SET name = ?
WHERE id = ?;

-- name: UpdateCourseTitle :exec
UPDATE course SET title = ?
WHERE id = ?;

-- name: DeleteStudent :exec
DELETE FROM student 
WHERE id = ?;

-- name: DeleteCourse :exec
DELETE FROM course 
WHERE id = ?;

-- name: DeleteEnrollmentByCourseID :exec
DELETE FROM enrollment 
WHERE course_id = ?;

務必注意:針對不同的數據庫,queries.sql 中使用的語法有所不同,關於 queries.sql 的 DSL 語法形式的詳細內容,可參考 sqlc docs[13]。

5.4 生成代碼

在項目 sqlc 根目錄下運行下面 sqlc 命令可以在指定的 db 目錄下生成包名爲 db 的 Go 代碼:

$sqlc generate
$tree db
db
├── db.go
├── models.go
└── queries.sql.go

其中 queries.sql.go 是對應 queries.sql 中所有 dml 操作的方法。下面摘錄 queries.sql.go 的代碼片段:

// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.26.0
// source: queries.sql

package db

import (
 "context"
 "database/sql"
)

const createCourse = `-- name: CreateCourse :execresult
INSERT INTO course (
  title, dept_id
) VALUES (
  ?, ?
)
`

type CreateCourseParams struct {
 Title  string
 DeptID sql.NullInt32
}

func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (sql.Result, error) {
 return q.db.ExecContext(ctx, createCourse, arg.Title, arg.DeptID)
}

const createDepartment = `-- name: CreateDepartment :execresult
INSERT INTO department (
  name
) VALUES (
  ?
)
`

func (q *Queries) CreateDepartment(ctx context.Context, name string) (sql.Result, error) {
 return q.db.ExecContext(ctx, createDepartment, name)
}
... ...

我們看到,queries.sql 中的操作都以 Queries 類型的方法形式存在,在後面的使用過程中,我們可以體會這種方式帶來的編碼時的便利。

5.5 使用生成的代碼訪問和操作數據庫

下面是使用生成的 Go 代碼進行數據庫操作的示例,我們需要先初始化數據庫連接並創建 Queries 實例,然後基於創建的 Queries 實例的方法進行數據庫表操作:

// database-access/sqlc/main.go

package main

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

 "demo/db"

 _ "github.com/go-sql-driver/mysql"
)

func main() {
 dsn := "root:123456@tcp(127.0.0.1:4407)/example_db"
 conn, err := sql.Open("mysql", dsn)
 if err != nil {
  log.Fatal(err)
 }
 defer conn.Close()

 queries := db.New(conn)

 // 執行CRUD操作
 createData(queries)
 queryData(queries)
 updateData(queries)
 deleteData(queries)
}

func createData(queries *db.Queries) {
 ctx := context.Background()

 // 創建部門
 _, err := queries.CreateDepartment(ctx, "Computer Science")
 if err != nil {
  log.Fatal(err)
 }
 _, err = queries.CreateDepartment(ctx, "Mathematics")
 if err != nil {
  log.Fatal(err)
 }

 // 創建教師
 _, err = queries.CreateInstructor(ctx, db.CreateInstructorParams{Name: "John Doe", DeptID: sql.NullInt32{1, true}})
 if err != nil {
  log.Fatal(err)
 }
 _, err = queries.CreateInstructor(ctx, db.CreateInstructorParams{Name: "Jane Smith", DeptID: sql.NullInt32{2, true}})
 if err != nil {
  log.Fatal(err)
 }

 // 創建課程
 _, err = queries.CreateCourse(ctx, db.CreateCourseParams{Title: "Database Systems", DeptID: sql.NullInt32{1, true}})
 if err != nil {
  log.Fatal(err)
 }
 _, err = queries.CreateCourse(ctx, db.CreateCourseParams{Title: "Calculus", DeptID: sql.NullInt32{2, true}})
 if err != nil {
  log.Fatal(err)
 }

 // 創建學生
 _, err = queries.CreateStudent(ctx, db.CreateStudentParams{Name: "Alice", DeptID: sql.NullInt32{1, true}})
 if err != nil {
  log.Fatal(err)
 }
 _, err = queries.CreateStudent(ctx, db.CreateStudentParams{Name: "Bob", DeptID: sql.NullInt32{2, true}})
 if err != nil {
  log.Fatal(err)
 }

 // 學生選課
 _, err = queries.EnrollStudent(ctx, db.EnrollStudentParams{StudentID: sql.NullInt32{1, true}, CourseID: sql.NullInt32{1, true}, Semester: "Fall", Year: 2024})
 if err != nil {
  log.Fatal(err)
 }
 _, err = queries.EnrollStudent(ctx, db.EnrollStudentParams{StudentID: sql.NullInt32{2, true}, CourseID: sql.NullInt32{2, true}, Semester: "Fall", Year: 2024})
 if err != nil {
  log.Fatal(err)
 }
}

func queryData(queries *db.Queries) {
 ctx := context.Background()

 // 查詢所有學生
 students, err := queries.GetStudents(ctx)
 if err != nil {
  log.Fatal(err)
 }
 for _, student := range students {
  fmt.Printf("Student ID: %d, Name: %s, Department ID: %d\n", student.ID, student.Name, student.DeptID.Int32)
 }

 // 查詢某個部門的課程
 courses, err := queries.GetCoursesByDept(ctx, sql.NullInt32{1, true})
 if err != nil {
  log.Fatal(err)
 }
 for _, course := range courses {
  fmt.Printf("Course ID: %d, Title: %s, Department ID: %d\n", course.ID, course.Title, course.DeptID.Int32)
 }

 // 查詢某個學生的選課信息
 enrollments, err := queries.GetEnrollmentByStudent(ctx, sql.NullInt32{1, true})
 if err != nil {
  log.Fatal(err)
 }
 for _, enrollment := range enrollments {
  fmt.Printf("Student ID: %d, Course ID: %d, Semester: %s, Year: %d\n", enrollment.StudentID.Int32, enrollment.CourseID.Int32, enrollment.Semester, enrollment.Year)
 }
}

func updateData(queries *db.Queries) {
 ctx := context.Background()

 // 更新學生姓名
 err := queries.UpdateStudentName(ctx, db.UpdateStudentNameParams{ID: 1, Name: "Alice Johnson"})
 if err != nil {
  log.Fatal(err)
 }

 // 更新課程標題
 err = queries.UpdateCourseTitle(ctx, db.UpdateCourseTitleParams{ID: 1, Title: "Advanced Database Systems"})
 if err != nil {
  log.Fatal(err)
 }
}

func deleteData(queries *db.Queries) {
 ctx := context.Background()

 // 刪除選課記錄
 err := queries.DeleteEnrollmentByCourseID(ctx, sql.NullInt32{1, true})
 if err != nil {
  log.Fatal(err)
 }

 // 刪除課程
 err = queries.DeleteCourse(ctx, 1)
 if err != nil {
  log.Fatal(err)
 }

 // 刪除學生
 err = queries.DeleteStudent(ctx, 1)
 if err != nil {
  log.Fatal(err)
 }
}

通過上述示例,我們可以看到 sqlc 在生成類型安全的 Go 代碼方面非常高效,它結合了 SQL 查詢的靈活性和 Go 語言的類型安全特性,使得數據庫操作更加直觀和可靠。不過,學習 sqlc 的 DSL 還是需要一點時間的,也有一個小小的門檻。

除了 sqlc,Facebook 開源的 entgo 是一個同時基於代碼生成以及 ORM 進行數據庫操作的方式。和 sqlc 一樣,entgo 在前期需要一定的額外學習成本。下面我們來看看使用 entgo 如何訪問和操作數據庫。

5.6. 使用 entgo 操作數據庫

Ent 是 Facebook 開源的一個實體框架,它使用 Schema 作爲強類型的 Go 代碼生成數據模型和查詢方法。Ent 提供了類型安全的 API、自動化的遷移、GraphQL 支持等特性。

5.6.1 安裝 Ent

和 sqlc 一樣,由於需要代碼生成,我們需要先安裝 ent 的命令行工具:

$go install  entgo.io/ent/cmd/ent@latest

使用下面命令可以驗證 ent 安裝是否 ok:

$ent -h
Usage:
  ent [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  describe    print a description of the graph schema
  generate    generate go code for the schema directory
  help        Help about any command
  new         initialize a new environment with zero or more schemas

Flags:
  -h, --help   help for ent

Use "ent [command] --help" for more information about a command.

接下來,和 sqlc 一樣,我們需要使用 ent 的 DSL 來定義 schema,和 sqlc 不同,ent 使用 Go 語法來定義 schema。

5.6.2 定義 Schema

使用 Ent 需要先定義 Schema,我們建立一個 schema 目錄,將所有 schema 相關的 Go 定義文件都放入目錄中:

$tree schema 
schema
├── course.go
├── department.go
├── enrollment.go
├── instructor.go
└── student.go

schema 目錄下的每個文件都是一個 entity 的定義,以 department.go 爲例:

// database-access/ent/schema/department.go
package schema
  
import (
    "entgo.io/ent"
    "entgo.io/ent/schema/edge"
    "entgo.io/ent/schema/field"
)

// Department holds the schema definition for the Department entity.
type Department struct {
    ent.Schema
}

// Fields of the Department.
func (Department) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").NotEmpty(),
    }
}

// Edges of the Department.
func (Department) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("instructors", Instructor.Type),
        edge.To("courses", Course.Type),
        edge.To("students", Student.Type),
    }
}

我們看到結構體類型 Department 對應表 department,department 與其他表之間的關係使用 ent.Edge 表示,這樣就建立了與其他表的關係。有了 Schema 定義後,我們就可以來生成代碼了。

5.6.3 生成代碼

我們在 database-access/ent 目錄下執行下面命令:

$ent generate demo/schema --target ent

ent 會基於 demo/schema 包生成相應代碼 (這裏 go module 爲 demo),即在 ent 目錄下生成包名爲 ent 的代碼:

$tree -L 1 -F ./ent
./ent
├── client.go
├── course/
├── course.go
├── course_create.go
├── course_delete.go
├── course_query.go
├── course_update.go
├── department/
├── department.go
├── department_create.go
├── department_delete.go
├── department_query.go
├── department_update.go
├── enrollment/
├── enrollment.go
├── enrollment_create.go
├── enrollment_delete.go
├── enrollment_query.go
├── enrollment_update.go
├── ent.go
├── enttest/
├── hook/
├── instructor/
├── instructor.go
├── instructor_create.go
├── instructor_delete.go
├── instructor_query.go
├── instructor_update.go
├── migrate/
├── mutation.go
├── predicate/
├── runtime/
├── runtime.go
├── student/
├── student.go
├── student_create.go
├── student_delete.go
├── student_query.go
├── student_update.go
└── tx.go

我們看到,ent 爲每個 entity,比如 department 都生成了一組文件,包括增刪改查。接下來,我們就來使用 ent 生成的代碼來操作數據庫!

5.6.4 使用生成的代碼操作數據庫

下面是使用 ent 生成的代碼操作數據庫的示例代碼:

// database-access/ent/main.go
package main

import (
 "context"
 "log"

 "demo/ent"
 "demo/ent/course"
 "demo/ent/department"
 "demo/ent/enrollment"
 "demo/ent/student"

 _ "github.com/go-sql-driver/mysql"
)

func main() {
 client, err := ent.Open("mysql""root:123456@tcp(127.0.0.1:4407)/example_db?parseTime=True")
 if err != nil {
  log.Fatalf("failed opening connection to mysql: %v", err)
 }
 defer client.Close()
 ctx := context.Background()

 // Run the automatic migration tool to create all schema resources.
 if err := client.Schema.Create(ctx); err != nil {
  log.Fatalf("failed creating schema resources: %v", err)
 }

 // 執行CRUD操作
 createData(ctx, client)
 queryData(ctx, client)
 updateData(ctx, client)
 deleteData(ctx, client)
}

func createData(ctx context.Context, client *ent.Client) {
 // 創建部門
 cs, err := client.Department.Create().SetName("Computer Science").Save(ctx)
 if err != nil {
  log.Fatal(err)
 }
 math, err := client.Department.Create().SetName("Mathematics").Save(ctx)
 if err != nil {
  log.Fatal(err)
 }

 // 創建教師
 _, err = client.Instructor.Create().SetName("John Doe").SetDepartment(cs).Save(ctx)
 if err != nil {
  log.Fatal(err)
 }
 _, err = client.Instructor.Create().SetName("Jane Smith").SetDepartment(math).Save(ctx)
 if err != nil {
  log.Fatal(err)
 }

 // 創建課程
 dbCourse, err := client.Course.Create().SetTitle("Database Systems").SetDepartment(cs).Save(ctx)
 if err != nil {
  log.Fatal(err)
 }
 calcCourse, err := client.Course.Create().SetTitle("Calculus").SetDepartment(math).Save(ctx)
 if err != nil {
  log.Fatal(err)
 }

 // 創建學生
 alice, err := client.Student.Create().SetName("Alice").SetDepartment(cs).Save(ctx)
 if err != nil {
  log.Fatal(err)
 }
 bob, err := client.Student.Create().SetName("Bob").SetDepartment(math).Save(ctx)
 if err != nil {
  log.Fatal(err)
 }

 // 學生選課
 _, err = client.Enrollment.Create().SetStudent(alice).SetCourse(dbCourse).SetSemester("Fall").SetYear(2024).Save(ctx)
 if err != nil {
  log.Fatal(err)
 }
 _, err = client.Enrollment.Create().SetStudent(bob).SetCourse(calcCourse).SetSemester("Fall").SetYear(2024).Save(ctx)
 if err != nil {
  log.Fatal(err)
 }
}

func queryData(ctx context.Context, client *ent.Client) {
 // 查詢所有學生
 //students, err := client.Student.Query().All(ctx)
 students, err := client.Student.Query().WithDepartment().All(ctx)
 if err != nil {
  log.Fatal(err)
 }
 for _, stu := range students {
  log.Printf("Student ID: %d, Name: %s, Department ID: %d\n", stu.ID, stu.Name, stu.Edges.Department.ID)
 }

 // 查詢某個部門的課程
 courses, err := client.Course.Query().WithDepartment().Where(course.HasDepartmentWith(department.ID(1))).All(ctx)
 if err != nil {
  log.Fatal(err)
 }
 for _, course := range courses {
  log.Printf("Course ID: %d, Title: %s, Department ID: %d\n", course.ID, course.Title, course.Edges.Department.ID)
 }

 // 查詢某個學生的選課信息
 enrollments, err := client.Enrollment.Query().WithStudent().WithCourse().Where(enrollment.HasStudentWith(student.ID(1))).All(ctx)
 if err != nil {
  log.Fatal(err)
 }
 for _, enrollment := range enrollments {
  log.Printf("Student ID: %d, Course ID: %d, Semester: %s, Year: %d\n", enrollment.Edges.Student.ID,
   enrollment.Edges.Course.ID, enrollment.Semester, enrollment.Year)
 }
}

func updateData(ctx context.Context, client *ent.Client) {
 // 更新學生姓名
 _, err := client.Student.UpdateOneID(1).SetName("Alice Johnson").Save(ctx)
 if err != nil {
  log.Fatal(err)
 }

 // 更新課程標題
 _, err = client.Course.UpdateOneID(1).SetTitle("Advanced Database Systems").Save(ctx)
 if err != nil {
  log.Fatal(err)
 }
}

func deleteData(ctx context.Context, client *ent.Client) {
 // 刪除選課記錄
 _, err := client.Enrollment.Delete().Where(enrollment.HasCourseWith(course.ID(1))).Exec(ctx)
 if err != nil {
  log.Fatal(err)
 }

 // 刪除課程
 err = client.Course.DeleteOneID(1).Exec(ctx)
 if err != nil {
  log.Fatal(err)
 }

 // 刪除學生
 err = client.Student.DeleteOneID(1).Exec(ctx)
 if err != nil {
  log.Fatal(err)
 }
}

通過以上示例可以看到,使用 GORM 和 Ent 都可以大大簡化數據庫操作,並提供了類型安全的 API 和自動化的遷移支持,使得開發更加高效和可靠。

到這裏我們已經見識到了三類數據庫訪問和操作的方式,那麼究竟那種適合我們呢?我們接下來做一個簡單的對比。

  1. 不同數據庫訪問方式的對比

在前面的小節中,我們介紹了三種主要的數據庫訪問方式:Go 標準庫、ORM 庫(GORM),以及代碼生成工具(sqlc 和 ent)。在本節中,我們將基於前面示例中的表現,對這些方式進行一個簡單的對比,以幫助開發者在實際項目中做出最佳選擇。

以下是整理的關於 Go 不同數據庫訪問方式優缺點的表格:

這張表格總結了不同數據庫訪問方式的優缺點,幫助讀者選擇最適合其項目需求的方式。

關於各種數據庫訪問方式的性能對比,做起來還是稍麻煩的,之前 goland 博客曾發表過一篇有關 go 標準庫 vs. gorm vs. sqlx. vs. sqlc 的壓測的文章 [14],大家可以參考其中的結論,即 Go 標準庫、sqlc 由於是原生 sql 操作,所以性能最佳;sqlx 略有擴展,性能次之;gorm 在小數據量的情況下,性能是很快的,但性能會隨着數據量的增加而下降很多。

綜合,以上對比與性能情況,這裏也給出一些選擇建議:

  1. 小結

本文詳細介紹了在 Go 語言中訪問和操作數據庫的幾種主流方式。

我們首先搭建了一個基於 MySQL 數據庫的示例環境,並定義了一個簡單的學校院系選課關係模型作爲數據庫模式。然後,我們分別使用以下三種方法實現了對該數據庫的 CRUD 操作:

最後,我們還簡單對比了這三種方法的優劣,希望可以幫助大家選擇出適合自身項目的數據庫訪問方式。

本文涉及的源碼可以在這裏 [15] 下載 - https://github.com/bigwhite/experiments/blob/master/database-access

本文中的部分源碼由 OpenAI 的 GPT-4o 生成。

  1. 參考資料


Gopher Daily(Gopher 每日新聞) - https://gopherdaily.tonybai.com

我的聯繫方式:

參考資料

[1] 

sqlx: https://github.com/jmoiron/sqlx

[2] 

GORM: https://github.com/go-gorm/gorm

[3] 

sqlc: https://github.com/sqlc-dev/sqlc

[4] 

Ent: https://github.com/ent/ent

[5] 

MySQL 數據庫: https://www.mysql.com/

[6] 

《Database System Concepts,7th》: https://book.douban.com/subject/35501216/

[7] 

go-sql-driver/mysql: github.com/go-sql-driver/mysql

[8] 

sqlx: https://github.com/jmoiron/sqlx

[9] 

xorm: https://gitea.com/xorm/xorm

[10] 

cmdline 工具: https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go

[11] 

dsl 語法: https://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1

[12] 

yaml 格式文件: https://tonybai.com/2019/02/25/introduction-to-yaml-creating-a-kubernetes-deployment/

[13] 

sqlc docs: https://docs.sqlc.dev/en/latest/index.html

[14] 

有關 go 標準庫 vs. gorm vs. sqlx. vs. sqlc 的壓測的文章: https://blog.jetbrains.com/zh-hans/go/2023/06/30/database-sql-gorm-sqlx-sqlc/

[15] 

這裏: https://github.com/bigwhite/experiments/blob/master/gdatabase-access

[16] 

“Gopher 部落” 知識星球: https://public.zsxq.com/groups/51284458844544

[17] 

鏈接地址: https://m.do.co/c/bff6eed92687

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/uUqWzabm6yoKuz3rhAvrpA