注意:這個倉庫目前已經被中止維護,並轉為單純的 SQL Builder(SQL 建置工具)了:https://github.com/teacat/rushia
注意:這個倉庫目前已經被中止維護,並轉為單純的 SQL Builder(SQL 建置工具)了:https://github.com/teacat/rushia
一個由 Golang 撰寫且比起部分 ORM 還要讚的 MySQL 指令建置函式庫。彈性高、不需要建構體標籤。實際上,這就只是 PHP-MySQLi-Database-Class 不過是用在 Golang 而已(但還是多了些功能)。
萊納是一個由 Golang 撰寫的 MySQL 的指令建置函式庫(不是 ORM,永遠也不會是),幾乎所有東西都能操控於你手中。類似自己撰寫資料庫指令但是更簡單,JOIN 表格也變得比以前更方便了。
- 幾乎全功能的函式庫。
- 自動避免於 Goroutine 發生資料競爭的設計。
- 支援 MySQL 複寫橫向擴展機制(區分讀/寫連線)。
- 容易理解與記住、且使用方式十分簡單。
- SQL 指令建構函式。
- 資料庫表格建構協助函式。
- 可串連的使用方式。
- 支援子指令(Sub Query)。
- 支援多樣的結果物件綁定(如:建構體切片)。
- 可手動操作的交易機制(Transaction)和回溯(Rollback)功能。
- 透過預置聲明(Prepared Statement),99.9% 避免 SQL 注入攻擊。
Gorm 已經是 Golang 裡的 ORM 典範,但實際上要操作複雜與關聯性高的 SQL 指令時並不是很合適,而 Reiner 解決了這個問題。Reiner 也試圖不要和建構體扯上關係,不希望使用者需要手動指定任何標籤在建構體中。
我們都知道 Golang 的目標就是併發程式,為了避免 Goroutine 導致資料競爭問題,Reiner 會在每有變更的時候自動複製 SQL 指令建置函式庫來避免所有併發程式共用同個 SQL 指令建置函式庫(此方式並不會使資料庫連線遞增而造成效能問題)。
在原先的舊版本中則需要手動透過 Copy
或 Clone
複製建置函式庫,這繁雜的手續正是重新設計的原因。但也因為如此,現在 SQL 指令建置函式若需要分散串接則需要重新地不斷賦值,簡單來說就是像這樣。
package main
func main() {
db, _ := reiner.New("...")
// 在舊有的版本中原本能夠這樣直覺地分散串接一段 SQL 指令。
// 注意!這是舊版本的做法,目前已經被廢除。
db.Table("Users")
if ... {
db.Where("Username", "YamiOdymel")
}
if ... {
db.Limit(1, 10)
}
db.Get()
// 新的版本中因為 Reiner 會不斷地回傳一個新的建置資料函式,
// 因此必須不斷地重新賦值。
myDB := db.Table("Users")
if ... {
myDB = myDB.Where("Username", "YamiOdymel")
}
if ... {
myDB = myDB.Limit(1, 10)
}
myDB, _ = myDB.Get()
}
這裡有份簡略化的效能測試報表。目前仍會持續優化並且增加快取以避免重複建置相同指令而費時。
測試規格:
1.7 GHz Intel Core i7 (4650U)
8 GB 1600 MHz DDR3
插入:Dbr > SQL > SQLx > Xorm > Reiner > Gorm
BenchmarkReinerInsert-4 3000 571298 ns/op 1719 B/op 49 allocs/op
BenchmarkSQLInsert-4 3000 429340 ns/op 901 B/op 17 allocs/op
BenchmarkDbrInsert-4 5000 413442 ns/op 2210 B/op 37 allocs/op
BenchmarkSQLxInsert-4 3000 444055 ns/op 902 B/op 17 allocs/op
BenchmarkGormInsert-4 2000 776838 ns/op 5319 B/op 101 allocs/op
BenchmarkXormInsert-4 3000 562341 ns/op 2921 B/op 64 allocs/op
選擇 100 筆資料:SQL > SQLx > Dbr > Reiner > Gorm > Xorm
BenchmarkReinerSelect100-4 2000 659189 ns/op 42907 B/op 1155 allocs/op
BenchmarkSQLSelect100-4 5000 336121 ns/op 28864 B/op 723 allocs/op
BenchmarkDbrSelect100-4 3000 529430 ns/op 87496 B/op 1638 allocs/op
BenchmarkSQLxSelect100-4 3000 376810 ns/op 32368 B/op 829 allocs/op
BenchmarkGormSelect100-4 2000 726107 ns/op 209236 B/op 3870 allocs/op
BenchmarkXormSelect100-4 2000 868688 ns/op 103358 B/op 4583 allocs/op
- 安裝方式
- 命名建議
- NULL 值
- 使用方式
- 表格建構函式
打開終端機並且透過 go get
安裝此套件即可。
$ go get gopkg.in/teacat/reiner.v2
在 Reiner 中為了配合 Golang 程式命名規範,我們建議你將所有事情以駝峰式大小寫命名,因為這能夠確保兩邊的風格相同。事實上,甚至連資料庫內的表格名稱、欄位名稱都該這麼做。當遇上 ip
、id
、url
時,請遵循 Golang 的命名方式皆以大寫使用,如 AddrIP
、UserID
、PhotoURL
,而不是 AddrIp
、UserId
、PhotoUrl
。
在 Golang 裏處理資料庫的 NULL 值向來都不是很方便,因此不建議允許資料庫中可有 NULL 欄位。基於 Reiner 底層的 go-sql-driver/mysql
因素,Reiner 並不會將接收到的 NULL 值轉換成指定型態的零值(Zero Value),這意味著當你從資料庫中取得一個可能為 NULL 值的字串,你必須透過 *string
或者 sql.NullString
而非普通的 string
型態(會發生 Scan 錯誤),實際用法像這樣。
type User struct {
Username string
Nickname sql.NullString
Age sql.NullInt64
}
// 然後綁定這個建構體到資料庫結果。
var u User
db.Table("Users").Bind(&u).GetOne()
// 輸出取得的結果。
if !u.Nickname.Valid() {
panic("Nickname 的內容是 NULL!不可饒恕!")
}
fmt.Println(u.Nickname.Value)
Reiner 的使用方式十分直覺與簡易,類似基本的 SQL 指令集但是更加地簡化了。
首先你需要透過函式來將 Reiner 連上資料庫,如此一來才能夠初始化建置函式庫與相關的資料庫表格建構函式。一個最基本的單資料庫連線,讀寫都將透過此連線,連線字串共用於其它套件是基於 DSN(Data Source Name)。
import "github.com/teacat/reiner"
db, err := reiner.New("root:root@/test?charset=utf8")
if err != nil {
panic(err)
}
這種方式可以有好幾個主要資料庫、副從資料庫,這意味著寫入時都會流向到主要資料庫,而讀取時都會向副從資料庫請求。這很適合用在大型結構還有水平擴展上。當你有多個資料庫來源時,Reiner 會逐一遞詢每個資料庫來源,英文稱其為 Round Robin,也就是每個資料庫都會輪流呼叫而避免單個資料庫負荷過重,也不會有隨機呼叫的事情發生。
import "github.com/teacat/reiner"
db, err := reiner.New("root:root@/master?charset=utf8", []string{
"root:root@/slave?charset=utf8",
"root:root@/slave2?charset=utf8",
"root:root@/slave3?charset=utf8",
})
if err != nil {
panic(err)
}
如果你已經有喜好的 SQL 資料庫處理套件,那麼你就可以在建立 Reiner 時不要傳入任何資料,這會使 Reiner 避免與資料庫互動,透過這個設計你可以將 Reiner 作為你的 SQL 指令建構函式。
// 當沒有傳入 MySQL 連線資料時,Reiner 僅會建置 SQL 執行指令而非與資料庫有實際互動。
builder, _ := reiner.New()
// 然後像這樣透過 Reiner 建立執行指令。
myQuery, _ := builder.Table("Users").Where("Username", "YamiOdymel").Get()
// 透過 `Query` 取得 Reiner 所建立的 Query 當作欲執行的資料庫指令。
sql.Prepare(myQuery.Query())
// 接著展開 `Params` 即是我們在 Reiner 中存放的值。
sql.Exec(myQuery.Params()...)
// 等效於:SELECT * FROM Users WHERE Username = ?
Reiner 允許你將查詢結果映射到結構體切片或結構體。
var user []*User
db.Bind(&user).Table("Users").Get()
透過 Reiner 你可以很輕鬆地透過建構體或是 map 來插入一筆資料。這是最傳統的插入方式,若該表格有自動遞增的編號欄位,插入後你就能透過 LastInsertID
獲得最後一次插入的編號。
db.Table("Users").Insert(map[string]interface{}{
"Username": "YamiOdymel",
"Password": "test",
})
// 等效於:INSERT INTO Users (Username, Password) VALUES (?, ?)
覆蓋的用法與插入相同,當有同筆資料時會先進行刪除,然後再插入一筆新的,這對有外鍵的表格來說十分危險。
db.Table("Users").Replace(map[string]interface{}{
"Username": "YamiOdymel",
"Password": "test",
})
// 等效於:REPLACE INTO Users (Username, Password) VALUES (?, ?)
插入時你可以透過 Reiner 提供的函式來執行像是 SHA1()
或者取得目前時間的 NOW()
,甚至將目前時間加上一年⋯等。
db.Table("Users").Insert(map[string]interface{}{
"Username": "YamiOdymel",
"Password": db.Func("SHA1(?)", "secretpassword+salt"),
"Expires": db.Now("+1Y"),
"CreatedAt": db.Now(),
})
// 等效於:INSERT INTO Users (Username, Password, Expires, CreatedAt) VALUES (?, SHA1(?), NOW() + INTERVAL 1 YEAR, NOW())
Reiner 支援了插入資料若重複時可以更新該筆資料的指定欄位>這類似「覆蓋」,但這並不會先刪除原先的資料,這種方式僅會在插入時檢查是否重複,若重複則更新該筆資料。
lastInsertID := "ID"
db.Table("Users").OnDuplicate([]string{"UpdatedAt"}, lastInsertID).Insert(map[string]interface{}{
"Username": "YamiOdymel",
"Password": "test",
"UpdatedAt": db.Now(),
})
// 等效於:INSERT INTO Users (Username, Password, UpdatedAt) VALUES (?, ?, NOW()) ON DUPLICATE KEY UPDATE UpdatedAt = VALUES(UpdatedAt)
Reiner 允許你透過 InsertMulti
同時間插入多筆資料(單指令插入多筆資料),這省去了透過迴圈不斷執行單筆插入的困擾,這種方式亦大幅度提升了效能。
data := []map[string]interface{}{
{
"Username": "YamiOdymel",
"Password": "test",
}, {
"Username": "Karisu",
"Password": "12345",
},
}
db.Table("Users").InsertMulti(data)
// 等效於:INSERT INTO Users (Username, Password) VALUES (?, ?), (?, ?)
Limit
能夠限制 SQL 執行的筆數,如果是 10,那就表示只處理最前面 10 筆資料而非全部(例如:選擇、更新、移除)。
db.Table("Users").Limit(10).Update(data)
// 等效於:UPDATE Users SET ... LIMIT 10
更新一筆資料在 Reiner 中極為簡單,你只需要指定表格名稱還有資料即可。
db.Table("Users").Where("Username", "YamiOdymel").Update(map[string]interface{}{
"Username": "Karisu",
"Password": "123456",
})
// 等效於:UPDATE Users SET Username = ?, Password = ? WHERE Username = ?
最基本的選擇在 Reiner 中稱之為 Get
而不是 Select
。
db.Table("Users").Get()
// 等效於:SELECT * FROM Users
在 Get
中傳遞欄位名稱作為參數,多個欄位由逗點區分,亦能是函式。
db.Table("Users").Get("Username", "Nickname")
// 等效於:SELECT Username, Nickname FROM Users
db.Table("Users").Get("COUNT(*) AS Count")
// 等效於:SELECT COUNT(*) AS Count FROM Users
通常多筆結果會映射到一個切片或是陣列,而 GetOne
可以取得單筆資料並將其結果映射到單個建構體或 map
,令使用上更加方便。
當透過 map[string]interface{}
當作映射對象的時候,請注意資料庫並不會自動辨別 int
、string
等資料型態,反倒有可能會是 int64
、[]uint8{[]byte}
,因此使用 map
時請多加注意在型態轉換上的部分。
var u User
db.Bind(&u).Table("Users").Where("ID", 1).GetOne()
// 等效於:SELECT * FROM Users WHERE ID = ? LIMIT 1
var d map[string]interface{}
db.Bind(&d).Table("Users").GetOne("SUM(ID) AS Sum", "COUNT(*) AS Count")
// 等效於:SELECT SUM(ID), COUNT(*) AS Count FROM Users LIMIT 1
fmt.Println(d["Sum"])
fmt.Println(d["Count"])
透過 GetValue
和 GetValues
來取得單個欄位的內容。例如說:你想要單個使用者的暱稱,甚至是多個使用者的暱稱陣列就很適用。
// 取得多筆資料的 `Username` 欄位資料。
var us []string
db.Bind(&u).Table("Users").GetValues("Username")
// 等效於:SELECT Username FROM Users
// 取得單筆資料的某個欄位值。
var u string
db.Bind(&u).Table("Users").GetValue("Username")
// 等效於:SELECT Username FROM Users LIMIT 1
// 或者是函式。
var i int
db.Bind(&i).Table("Users").GetValue("COUNT(*)")
// 等效於:SELECT COUNT(*) FROM Users LIMIT 1
分頁就像是取得資料ㄧ樣,但更擅長用於多筆資料、不會一次顯示完畢的內容。Reiner 能夠幫你自動處理換頁功能,讓你不需要自行計算換頁時的筆數應該從何開始。為此,你需要定義兩個變數,一個是目前的頁數,另一個是單頁能有幾筆資料。
// 目前的頁數。
page := 1
// 設置一頁最多能有幾筆資料。
db.PageLimit = 10
db = db.Table("Users").Paginate(page)
// 等效於:SELECT SQL_CALC_FOUND_ROWS * FROM Users LIMIT 0, 10
fmt.Println("目前頁數為 %d,共有 %d 頁", page, db.TotalPages)
Reiner 已經提供了近乎日常中 80% 會用到的方式,但如果好死不死你想使用的功能在那 20% 之中,我們還提供了原生的方法能讓你直接輸入 SQL 指令執行自己想要的鳥東西。一個最基本的生指令(Raw Query)就像這樣。
其中亦能帶有預置聲明(Prepared Statement),也就是指令中的問號符號替代了原本的值。這能避免你的 SQL 指令遭受注入攻擊。
var us []User
db.Bind(&us).RawQuery("SELECT * FROM Users WHERE ID >= ?", 10)
RawQueryOne
是個僅選擇單筆資料的生指令函式,這意味著你能夠將取得的資料映射到建構體或是 map
上。
var u User
db.Bind(&u).RawQueryOne("SELECT * FROM Users WHERE ID = ?", 10)
// 等效於:SELECT * FROM Users WHERE ID = ? LIMIT 1
var d map[string]interface{}
db.Bind(&d).RawQueryOne("SELECT SUM(ID), COUNT(*) AS Count FROM Users")
// 等效於:SELECT SUM(ID), COUNT(*) AS Count FROM Users LIMIT 1
fmt.Println(d["Sum"])
fmt.Println(d["Count"])
透過 RawQueryValue
與 RawQueryValues
可以取得單個欄位的內容。例如說:你想要單個使用者的暱稱,甚至是多個使用者的暱稱陣列就很適用。
// 取得多筆資料的 `Username` 欄位資料。
var us []string
db.Bind(&us).RawQueryValues("SELECT Username FROM Users")
// 取得單筆資料的某個欄位值。
var pwd string
db.Bind(&pwd).RawQueryValue("SELECT Password FROM Users WHERE ID = ?", 10)
// 等效於:SELECT Password FROM Users WHERE ID = ? LIMIT 1
// 或者是函式。
var i int
db.Bind(&i).RawQueryValue("SELECT COUNT(*) FROM Users")
// 等效於:SELECT COUNT(*) FROM Users LIMIT 1
如果你對 SQL 指令夠熟悉,你也可以使用更進階且複雜的用法。
db.RawQuery("SELECT ID, FirstName, LastName FROM Users WHERE ID = ? AND Username = ?", 1, "admin")
params := []int{10, 1, 10, 11, 2, 10}
query := (`
(SELECT A FROM t1 WHERE A = ? AND B = ?)
UNION ALL
(SELECT A FROM t2 WHERE A = ? AND B = ?)
UNION ALL
(SELECT A FROM t3 WHERE A = ? AND B = ?)
`)
db.RawQuery(query, params...)
透過 Reiner 宣告 WHERE
條件也能夠很輕鬆。一個最基本的 WHERE AND
像這樣使用。
db.Table("Users").Where("ID", 1).Where("Username", "admin").Get()
// 等效於:SELECT * FROM Users WHERE ID = ? AND Username = ?
HAVING
能夠與 WHERE
一同使用。
db.Table("Users").Where("ID", 1).Having("Username", "admin").Get()
// 等效於:SELECT * FROM Users WHERE ID = ? HAVING Username = ?
如果你想要在條件中宣告某個欄位是否等於某個欄位⋯你能夠像這樣。
// 別這樣。
db.Table("Users").Where("LastLogin", "CreatedAt").Get()
// 這樣才對。
db.Table("Users").Where("LastLogin = CreatedAt").Get()
// 等效於:SELECT * FROM Users WHERE LastLogin = CreatedAt
在 Where
或 Having
中,你可以自訂條件的運算子,如 >=、<=、<>⋯等。
db.Table("Users").Where("ID", ">=", 50).Get()
// 等效於:SELECT * FROM Users WHERE ID >= ?
條件也可以用來限制數值內容是否在某數之間(相反之,也能夠限制是否不在某範圍內)。
db.Table("Users").Where("ID", "BETWEEN", 0, 20).Get()
// 等效於:SELECT * FROM Users WHERE ID BETWEEN ? AND ?
條件能夠限制並確保取得的內容不在(或者在)指定清單內。
db.Table("Users").Where("ID", "IN", 1, 5, 27, -1, "d").Get()
// 等效於:SELECT * FROM Users WHERE ID IN (?, ?, ?, ?, ?)
list := []interface{}{1, 5, 27, -1, "d"}
db.Table("Users").Where("ID", "IN", list...).Get()
// 等效於:SELECT * FROM Users WHERE ID IN (?, ?, ?, ?, ?)
通常來說多個 Where
會產生 AND
條件,這意味著所有條件都必須符合,有些時候你只希望符合部分條件即可,就能夠用上 OrWhere
。
db.Table("Users").Where("FirstNamte", "John").OrWhere("FirstNamte", "Peter").Get()
// 等效於:SELECT * FROM Users WHERE FirstName = ? OR FirstName = ?
如果你的要求比較多,希望達到「A = B 或者 (A = C 或 A = D)」的話,你可以嘗試這樣。
db.Table("Users").Where("A = B").OrWhere("(A = C OR A = D)").Get()
// 等效於:SELECT * FROM Users WHERE A = B OR (A = C OR A = D)
確定某個欄位是否為空值。
// 別這樣。
db.Table("Users").Where("LastName", "NULL").Get()
// 這樣才對。
db.Table("Users").Where("LastName", "IS", nil).Get()
// 等效於:SELECT * FROM Users WHERE LastName IS NULL
Unix Timestamp 是一項將日期與時間秒數換算成數字的格式(範例:1498001308
),這令你能夠輕易地換算其秒數,但當你要判斷時間是否為某一年、月、日,甚至範圍的時候就會有些許困難,而 Reiner 也替你想到了這一點。
需要注意的是 Reiner 中的 Timestamp
工具無法串聯使用,這意味著當你想要確認時間戳是否為某年某月時,你需要有兩個 Where
條件,而不行使用 IsYear().IsMonth()
。更多的用法可以在原生文件中找到,這裡僅列出不完全的範例供大略參考。
判斷是否為特定年、月、日、星期或完整日期。
t := db.Timestamp
db.Table("Users").Where("CreatedAt", t.IsDate("2017-07-13")).Get()
// 等效於:SELECT * FROM Users WHERE DATE(FROM_UNIXTIME(CreatedAt)) = ?
db.Table("Users").Where("CreatedAt", t.IsYear(2017)).Get()
// 等效於:SELECT * FROM Users WHERE YEAR(FROM_UNIXTIME(CreatedAt)) = ?
db.Table("Users").Where("CreatedAt", t.IsMonth(1)).Get()
db.Table("Users").Where("CreatedAt", t.IsMonth("January")).Get()
// 等效於:SELECT * FROM Users WHERE MONTH(FROM_UNIXTIME(CreatedAt)) = ?
db.Table("Users").Where("CreatedAt", t.IsDay(16)).Get()
// 等效於:SELECT * FROM Users WHERE DAY(FROM_UNIXTIME(CreatedAt)) = ?
db.Table("Users").Where("CreatedAt", t.IsWeekday(5)).Get()
db.Table("Users").Where("CreatedAt", t.IsWeekday("Friday")).Get()
// 等效於:SELECT * FROM Users WHERE WEEKDAY(FROM_UNIXTIME(CreatedAt)) = ?
確定是否為特定時間。
t := db.Timestamp
db.Table("Users").Where("CreatedAt", t.IsHour(18)).Get()
// 等效於:SELECT * FROM Users WHERE HOUR(FROM_UNIXTIME(CreatedAt)) = ?
db.Table("Users").Where("CreatedAt", t.IsMinute(25)).Get()
// 等效於:SELECT * FROM Users WHERE MINUTE(FROM_UNIXTIME(CreatedAt)) = ?
db.Table("Users").Where("CreatedAt", t.IsSecond(16)).Get()
// 等效於:SELECT * FROM Users WHERE SECOND(FROM_UNIXTIME(CreatedAt)) = ?
db.Table("Users").Where("CreatedAt", t.IsWeekday(5)).Get()
// 等效於:SELECT * FROM Users WHERE WEEKDAY(FROM_UNIXTIME(CreatedAt)) = ?
你也能夠直接在條件中輸入指令。
db.Table("Users").Where("ID != CompanyID").Where("DATE(CreatedAt) = DATE(LastLogin)").Get()
// 等效於:SELECT * FROM Users WHERE ID != CompanyID AND DATE(CreatedAt) = DATE(LastLogin)
生條件中可以透過 ?
符號,並且在後面傳入自訂變數。
db.Table("Users").Where("(ID = ? OR ID = ?)", 6, 2).Where("Login", "Mike").Get()
// 等效於:SELECT * FROM Users WHERE (ID = ? OR ID = ?) AND Login = ?
刪除一筆資料再簡單不過了,透過 Count
計數能夠清楚知道你的 SQL 指令影響了幾行資料,如果是零的話即是無刪除任何資料。
var err error
db, err = db.Table("Users").Where("ID", 1).Delete()
if count := db.Count(); err == nil && count != 0 {
fmt.Printf("成功地刪除了 %d 筆資料!", count)
}
// 等效於:DELETE FROM Users WHERE ID = ?
Reiner 亦支援排序功能,如遞增或遞減,亦能擺放函式。
db.Table("Users").OrderBy("ID", "ASC").OrderBy("Login", "DESC").OrderBy("RAND()").Get()
// 等效於:SELECT * FROM Users ORDER BY ID ASC, Login DESC, RAND()
也能夠從值進行排序,只需要傳入一個切片即可。
db.Table("Users").OrderBy("UserGroup", "ASC", "SuperUser", "Admin", "Users").Get()
// 等效於:SELECT * FROM Users ORDER BY FIELD (UserGroup, ?, ?, ?) ASC
簡單的透過 GroupBy
就能夠將資料由指定欄位群組排序。
db.Table("Users").GroupBy("Name").Get()
// 等效於:SELECT * FROM Users GROUP BY Name
Reiner 支援多種表格加入方式,如:InnerJoin
、LeftJoin
、RightJoin
、NaturalJoin
、CrossJoin
。
db.
Table("Products").
LeftJoin("Users", "Products.TenantID = Users.TenantID").
Where("Users.ID", 6).
Get("Users.Name", "Products.ProductName")
// 等效於:SELECT Users.Name, Products.ProductName FROM Products AS Products LEFT JOIN Users AS Users ON (Products.TenantID = Users.TenantID) WHERE Users.ID = ?
你亦能透過 JoinWhere
或 JoinOrWhere
擴展表格加入的限制條件。
db.
Table("Products").
LeftJoin("Users", "Products.TenantID = Users.TenantID").
JoinOrWhere("Users", "Users.TenantID", 5).
Get("Users.Name", "Products.ProductName")
// 等效於:SELECT Users.Name, Products.ProductName FROM Products AS Products LEFT JOIN Users AS Users ON (Products.TenantID = Users.TenantID OR Users.TenantID = ?)
Reiner 支援複雜的子指令,欲要建立一個子指令請透過 SubQuery
函式,這將會建立一個不能被執行的資料庫建置函式庫,令你可以透過 Get
、Update
等建立相關 SQL 指令,但不會被資料庫執行。將其帶入到一個正常的資料庫函式中即可成為子指令。
subQuery := db.SubQuery().Table("Users").Get()
// 等效於不會被執行的:SELECT * FROM Users
你能夠輕易地將子指令放置在選擇/取得指令中。
subQuery := db.SubQuery().Table("Products").Where("Quantity", ">", 2).Get("UserID")
db.Table("Users").Where("ID", "IN", subQuery).Get()
// 等效於:SELECT * FROM Users WHERE ID IN (SELECT UserID FROM Products WHERE Quantity > ?)
插入新資料時也可以使用子指令。
subQuery := db.SubQuery().Table("Users").Where("ID", 6).Get("Name")
db.Table("Products").Insert(map[string]interface{}{
"ProductName": "測試商品",
"UserID": subQuery,
"LastUpdated": db.Now(),
})
// 等效於:INSERT INTO Products (ProductName, UserID, LastUpdated) VALUES (?, (SELECT Name FROM Users WHERE ID = 6), NOW())
就算是加入表格的時候也可以用上子指令,但你需要為子指令建立別名。
subQuery := db.SubQuery("Users").Table("Users").Where("Active", 1).Get()
db.
Table("Products").
LeftJoin(subQuery, "Products.UserID = U.ID").
Get("Users.Username", "Products.ProductName")
// 等效於:SELECT Users.Username, Products.ProductName FROM Products AS Products LEFT JOIN (SELECT * FROM Users WHERE Active = ?) AS Users ON Products.UserID = Users.ID
你同時也能夠透過子指令來確定某筆資料是否存在。
subQuery := db.SubQuery().Table("Users").Where("Company", "測試公司").Get("UserID")
db.Table("Products").Where(subQuery, "EXISTS").Get()
// 等效於:SELECT * FROM Products WHERE EXISTS (SELECT UserID FROM Users WHERE Company = ?)
有些時候我們只想知道資料庫是否有符合的資料,但並不是要取得其資料,舉例來說就像是登入是僅是要確認帳號密碼是否吻合,此時就可以透過 Has
用來確定資料庫是否有這筆資料。
has, err := db.Table("Users").Where("Username", "yamiodymel").Where("Password", "123456").Has()
if has {
fmt.Println("登入成功!")
} else {
fmt.Println("帳號或密碼錯誤。")
}
Reiner 有提供一些輔助用的函式協助你除錯、紀錄,或者更加地得心應手。
透過 Disconnect 結束一段連線。
if err := db.Disconnect(); err != nil {
panic(err)
}
你也能在資料庫發生錯誤、連線遺失時透過 Connect
來重新手動連線。
if err := db.Ping(); err != nil {
db.Connect()
}
取得最後一次所執行的 SQL 指令,這能夠用來記錄你所執行的所有動作。
db = db.Table("Users").Get()
fmt.Println("最後一次執行的 SQL 指令是:%s", db.LastQuery)
// 輸出:SELECT * FROM Users
行數很常用於檢查是否有資料、作出變更。資料庫不會因為沒有變更任何資料而回傳一個錯誤(資料庫僅會在真正發生錯誤時回傳錯誤資料),所以這是很好的檢查方法。
db, _ = db.Table("Users").Get()
fmt.Println("總共獲取 %s 筆資料", db.Count())
db, _ = db.Table("Users").Delete()
fmt.Println("總共刪除 %s 筆資料", db.Count())
db, _ = db.Table("Users").Update(data)
fmt.Println("總共更新 %s 筆資料", db.Count())
當插入一筆新的資料,而該表格帶有自動遞增的欄位時,就能透過 LastInsertID
取得最新一筆資料的編號。
var id int
db, _ = db.Table("Users").Insert(data)
id = db.LastInsertID
如果你是經由 InsertMulti
同時間插入多筆資料,基於 MySQL 底層的設定,你並沒有辦法透過 LastInsertID
取得剛才插入的所有資料編號。如果你仍希望取得插入編號,請透過迴圈不斷地執行 Insert
並保存其 LastInsertID
資料。
var ids []int
for ... {
var err error
db, err = db.Table("Users").Insert(data)
if err != nil {
ids = append(ids, db.LastInsertID)
}
}
如果你想取得這個指令總共能夠取得多少筆資料,透過 WithTotalCount
就能夠啟用總筆數查詢,這可能會稍微降低一點資料庫效能。
db, _ = db.Table("Users").WithTotalCount().Get()
fmt.Println(db.TotalCount)
交易函式僅限於 InnoDB 型態的資料表格,這能令你的資料寫入更加安全。你可以透過 Begin
開始記錄並繼續你的資料庫寫入行為,如果途中發生錯誤,你能透過 Rollback
回到紀錄之前的狀態,即為回溯(或滾回、退回),如果這筆交易已經沒有問題了,透過 Commit
將這次的變更永久地儲存到資料庫中。
// 當交易開始時請使用回傳的 `tx` 而不是原先的 `db`,這樣才能確保交易繼續。
tx, err := db.Begin()
if err != nil {
panic(err)
}
// 如果插入資料時發生錯誤,則呼叫 `Rollback()` 回到交易剛開始的時候。
if _, err = tx.Table("Wallets").Insert(data); err != nil {
tx.Rollback()
panic(err)
}
if _, err = tx.Table("Users").Insert(data); err != nil {
tx.Rollback()
panic(err)
}
// 透過 `Commit()` 確保上列變更都已經永久地儲存到資料庫。
if err := tx.Commit(); err != nil {
panic(err)
}
你能夠手動鎖定資料表格,避免同時間寫入相同資料而發生錯誤。
db.Table("Users").SetLockMethod("WRITE").Lock()
// 呼叫其他的 Lock() 函式也會自動將前一個上鎖解鎖,當然你也可以手動呼叫 Unlock() 解鎖。
db.Unlock()
// 等效於:UNLOCK TABLES
// 同時間要鎖上兩個表格也很簡單。
db.Table("Users", "Logs").SetLockMethod("READ").Lock()
// 等效於:LOCK TABLES Users READ, Logs READ
Reiner 也支援設置指令關鍵字。
db.Table("Users").SetQueryOption("LOW_PRIORITY").Insert(data)
// 等效於:INSERT LOW_PRIORITY INTO Users ...
db.Table("Users").SetQueryOption("FOR UPDATE").Get()
// 等效於:SELECT * FROM Users FOR UPDATE
db.Table("Users").SetQueryOption("SQL_NO_CACHE").Get()
// 等效於:SELECT SQL_NO_CACHE * FROM Users
你亦能同時設置多個關鍵字給同個指令。
db.Table("Users").SetQueryOption("LOW_PRIORITY", "IGNORE").Insert(data)
// Gives: INSERT LOW_PRIORITY IGNORE INTO Users ...
這會降低執行效能,但透過追蹤功能能夠有效地得知每個指令所花費的執行時間和建置指令,並且取得相關執行檔案路徑與行號。
db = db.SetTrace(true).Table("Users").Get()
fmt.Printf("%+v", db.Traces[0])
//[{Query:SELECT * FROM Users Duration:808.698µs Stacks:[map
//[File:/Users/YamiOdymel/go/src/github.com/teacat/reiner/builder.go Line:559 Skip:0 PC:19399228] map[Line:666 Skip:1 PC:19405153 //File:/Users/YamiOdymel/go/src/github.com/teacat/reiner/builder.go] map[Skip:2 PC:19407043 //File:/Users/YamiOdymel/go/src/github.com/teacat/reiner/builder.go Line:705] map[Line:74 Skip:3 PC:19548011 //File:/Users/YamiOdymel/go/src/github.com/teacat/reiner/builder.go] map[PC:17610310 //File:/usr/local/Cellar/go/1.8/libexec/src/testing/testing.go Line:657 Skip:4] map
//[File:/usr/local/Cellar/go/1.8/libexec/src/runtime/asm_amd64.s Line:2197 Skip:5 PC:17143345]] Error:<nil>}]
Reiner 除了基本的資料庫函式可供使用外,還能夠建立一個表格並且規劃其索引、外鍵、型態。
migration := db.Migration()
migration.Table("Users").Column("Username").Varchar(32).Primary().Create()
// 等效於:CREATE TABLE Users (Username VARCHAR(32) NOT NULL PRIMARY KEY) ENGINE=INNODB
數值 | 字串 | 二進制 | 檔案資料 | 時間 | 浮點數 | 固組 |
---|---|---|---|---|---|---|
TinyInt | Char | Binary | Blob | Date | Double | Enum |
SmallInt | Varchar | VarBinary | MediumBlob | DateTime | Decimal | Set |
MediumInt | TinyText | Bit | LongBlob | Time | Float | |
Int | Text | Timestamp | ||||
BigInt | MediumText | Year | ||||
LongText |
這裡是 Reiner 受啟發,或是和資料庫有所關聯的連結。