Skip to content
This repository was archived by the owner on Nov 27, 2023. It is now read-only.

Latest commit

 

History

History
664 lines (494 loc) · 24.4 KB

README-tw.md

File metadata and controls

664 lines (494 loc) · 24.4 KB

Rushia GoDoc Coverage Status Build Status Go Report Card

一個由 Golang 撰寫且比起部分 ORM 還要讚的 MySQL 指令建置函式庫。彈性高、不需要建構體標籤。原生想法基於 PHP-MySQLi-Database-ClassLaravel 查詢建構器 但多了些功能。

這是一個 SQL 指令建構庫,本身不帶有任何 SQL 連線,適合用於某些套件的基底。

特色

  • 幾乎全功能的函式庫。
  • 容易理解與記住、且使用方式十分簡單。
  • SQL 指令建構函式。
  • 資料庫表格建構協助函式。
  • 彈性的建構體映射。
  • 可串連的使用方式。
  • 支援子指令(Sub Query)。
  • 透過預置聲明(Prepared Statement),99.9% 避免 SQL 注入攻擊。

為什麼?

Gorm 已經是 Golang 裡的 ORM 典範,但實際上要操作複雜與關聯性高的 SQL 指令時並不是很合適,而 Rushia 解決了這個問題。Rushia 也試圖不要和建構體扯上關係,不希望使用者需要手動指定任何標籤在建構體中。

安裝方式

打開終端機並且透過 go get 安裝此套件即可。

$ go get github.com/teacat/rushia/v3

使用方式

Rushia 的使用方式十分直覺與簡易,類似基本的 SQL 指令集但是更加地簡化了。

初始化語法

最基本的資料庫執行語法起始於 NewQuery(...),其中可以帶入資料表的名稱,又或者是子指令(Sub Query)。而更為複雜的使用方式請參閱之後的章節。

q := rushia.NewQuery("Users")

複製語法

預設情況下你的所有修改總是會改到同一個 Rushia 語法,如果你的環境可能有多執行緒或是希望複製一份規則額外更改,請使用 Copy

a := rushia.NewQuery("Users")
a.Where("Type = ?", "VIP")

b := a.Copy()
b.Where("Name = ?", "YamiOdymel")

Build(a.Select())
// 等效於:SELECT * FROM Users WHERE Type = ?
Build(b.Select())
// 等效於:SELECT * FROM Users WHERE Type = ? AND Name = ?

建置語法

當完成撰寫一個查詢語法後,必須透過 Build 將其建置便能得到建置的語句與其參數。一個語句必須要有 SelectExistsReplaceUpdateDelete…等作為結尾,否則會無法建置。

query, params := rushia.Build(rushia.NewQuery("Users").Select())
// 等效於:SELECT * FROM Users

與其他資料庫套件搭配

由於 Rushia 是一個語法建置套件,這讓你可以得心應手地與自己喜好的資料庫連線函式庫進行搭配。舉例來說你可以使用 jmoiron/sqlx

// 初始化 SQLX 的連線。
db, err := sqlx.Open("mysql", "root:password@tcp(localhost:3306)/db")

// 透過 Rushia 建置語法。
q := rushia.NewQuery("Users").Where("Usernam = ?", "YamiOdymel").Select()
query, params := rushia.Build(q)

// 將相關語法與參數傳入給 SQLX 的函式並執行。
rows, err := db.Query(query, params...)
// 等效於:SELECT * FROM Users WHERE Username = ?

又或者是 go-gorm/gorm

// 初始化 Gorm 的連線。
db, err := gorm.Open(mysql.Open("root:password@tcp(localhost:3306)/db"), &gorm.Config{})

// 透過 Rushia 建置語法。
q := rushia.NewQuery("Users").Where("Username = ?", "YamiOdymel").Select()
query, params := rushia.Build(q)

// 將相關語法與參數傳入給 Gorm 的函式並執行。
db.Raw(query, params...).Scan(&myUser)
// 等效於:SELECT * FROM Users WHERE Username = ?

結構體映射

你能夠直接將一個建構體傳入 Insert 或是 Update 之中,其欄位名稱與值都會被自動轉換 (注意!這並不會轉換成 MySQL 最常用的 snake_case!)。

type User struct {
	Username string
	Password string
}
u := User{
	Username: "YamiOdymel",
	Password: "test",
}
rushia.NewQuery("Users").Insert(u)
// 等效於:INSERT INTO Users (Username, Password) VALUES (?, ?)

結構體標籤

透過指定 rushia 結構體標籤,你可以省略一個欄位或是重新命名其欄位在 MySQL 查詢語法裡的名稱。

type User struct {
	Username string `rushia:"-"`
	RealName string `rushia:"real_name"`
	Password string
}
u := User{
	Username: "YamiOdymel",
	RealName: "洨洨安",
	Password: "test",
}
rushia.NewQuery("Users").Insert(u)
// 等效於:INSERT INTO Users (real_name, Password) VALUES (?, ?)

省略

透過 Omit,你可以省略建構體中的某些欄位。

type User struct {
	Username string
	Password string
	Age      int    `rushia:"my_age"`
}
u := User{
	Username: "YamiOdymel",
	Password: "test",
	Age     : "32"
}
rushia.NewQuery("Users").Omit("Username", "my_age").Insert(u)
// 等效於:INSERT INTO Users (Password) VALUES (?)

插入

Rushia 提供一個簡短的 Hmap[string]interface{} 別名),這趨近於 gin.H。建立一個插入語法的時候可以傳入 HH 或是結構體。

rushia.NewQuery("Users").Insert(rushia.H{
	"Username": "YamiOdymel",
	"Password": "test",
})
// 等效於:INSERT INTO Users (Username, Password) VALUES (?, ?)

rushia.NewQuery("Users").Insert(map[string]interface{
	"Username": "YamiOdymel",
	"Password": "test",
})
// 等效於:INSERT INTO Users (Username, Password) VALUES (?, ?)

多筆資料

Rushia 允許你透過 []H[]map[string]interface{} 一次插入多筆資料。

data := []H{
	{
		"Username": "YamiOdymel",
		"Password": "test",
	}, {
		"Username": "Karisu",
		"Password": "12345",
	},
}
rushia.NewQuery("Users").Insert(data)
// 等效於:INSERT INTO Users (Username, Password) VALUES (?, ?), (?, ?)

覆蓋

覆蓋的用法與插入相同。當有同筆資料時會先進行刪除,然後再插入一筆新的,這對有外鍵的表格來說十分危險。若需要更為安全的方式請使用 OnDuplicateON DUPLICATE KEY UPDATE)函式。

rushia.NewQuery("Users").Replace(rushia.H{
	"Username": "YamiOdymel",
	"Password": "test",
})
// 等效於:REPLACE INTO Users (Username, Password) VALUES (?, ?)

當重複時

Rushia 支援了插入資料若重複時可以更新該筆資料的指定欄位,這類似「覆蓋」,但這並不會先刪除原先的資料,這種方式僅會在插入時檢查是否重複,若重複則更新該筆資料。

rushia.NewQuery("Users").As("New").OnDuplicate(rushia.H{
	"UpdatedAt": rushia.NewExpr("New.UpdatedAt"),
}).Insert(rushia.H{
	"Username":  "YamiOdymel",
	"UpdatedAt": rushia.NewExpr("NOW()"),
})
// 等效於:INSERT INTO Users (Username, UpdatedAt) VALUES (?, NOW()) AS New ON DUPLICATE KEY UPDATE UpdatedAt = New.UpdatedAt

rushia.NewQuery("Users").OnDuplicate(rushia.H{
	"UpdatedAt": rushia.NewExpr("VALUES(UpdatedAt)"),
}).Insert(rushia.H{
	"Username":  "YamiOdymel",
	"UpdatedAt": rushia.NewExpr("NOW()"),
})
// 注意!`VALUES` 這個用法已經在 MySQL 8.0.20 被棄用!請使用上面的方法!
// 等效於:INSERT INTO Users (Username, UpdatedAt) VALUES (?, NOW()) ON DUPLICATE KEY UPDATE UpdatedAt = VALUES(UpdatedAt)

表達式

插入較為複雜的值時,可以使用 NewExpr 建立一個新的表達式,便能傳入生指令與相關參數執行像是 SHA1() 或者取得目前時間的 NOW(),甚至將目前時間加上一年 ⋯ 等。

rushia.NewQuery("Users").Insert(rushia.H{
	"Username":  "YamiOdymel",
	"Password":  rushia.NewExpr("SHA1(?)", "secretpassword+salt"),
	"Expires":   rushia.NewExpr("NOW() + INTERVAL 1 YEAR"),
	"CreatedAt": rushia.NewExpr("NOW()"),
})
// 等效於:INSERT INTO Users (Username, Password, Expires, CreatedAt) VALUES (?, SHA1(?), NOW() + INTERVAL 1 YEAR, NOW())

筆數限制

Limit 能夠限制 SQL 執行的筆數,如果指定 10,那就表示只處理最前面 10 筆資料而非全部(例如:選擇、更新、移除)。如果指定 10, 20,那就是忽略前面 10 筆,並處理之後的 20 筆資料(11, 12... 30)。

rushia.NewQuery("Users").Limit(10).Update(data)
// 等效於:UPDATE Users SET ... LIMIT 10

rushia.NewQuery("Users").Limit(10, 20).Select(data)
// 等效於:SELECT * from Users LIMIT 10, 20

筆數偏移

透過 Offset 能夠以偏移的方式取得資料,這類似 Limit 但參數是反過來的。例如:10, 20 則會從 21 開始取得 10 筆資料(21, 22... 30)。

rushia.NewQuery("Users").Offset(10, 20).Select()
// 等效於:SELECT * from Users LIMIT 10 OFFSET 20

分頁

Paginate 是一個較親近於人類的友善好函式,其參數為:頁數, 單頁筆數。例如:1, 20 會取得首 20 筆資料,而 2, 20 則會取得第二頁的 20 筆資料(基本上為 21 至 40)。

rushia.NewQuery("Users").Paginate(1, 20).Select()
// 等效於:SELECT * from Users LIMIT 0, 20

rushia.NewQuery("Users").Paginate(2, 20).Select()
// 等效於:SELECT * from Users LIMIT 20, 20

更新

更新一筆資料在 Rushia 中極為簡單,你只需要指定表格名稱還有資料即可。

rushia.NewQuery("Users").Where("Username = ?", "YamiOdymel").Update(rushia.H{
	"Username": "Karisu",
	"Password": "123456",
})
// 等效於:UPDATE Users SET Username = ?, Password = ? WHERE Username = ?

片段更新

當你希望某些欄位在零值的時候不要進行更新,那麼你就可以使用 Patch 來做片段更新(也叫小修補)。

rushia.NewQuery("Users").Where("Username = ?", "YamiOdymel").Patch(rushia.H{
	"Age": 0,
	"Username": "",
	"Password": "123456",
})
// 等效於:UPDATE Users SET Password = ? WHERE Username = ?

如果你希望有些欄位雖然是零值(如:false0)但仍該在 Patch 時照樣更新,那麼就可以使用 Exclude。傳入資料型態(如:reflect.Boolreflect.String)來以型態排除特定欄位、而字串則表示欲忽略的欄位名稱。

排除的資料型態或欄位會在零值時一樣被更新到資料庫中。

rushia.NewQuery("Users").Where("Username = ?", "YamiOdymel").Exclude("Username", reflect.Int).Patch(rushia.H{
	"Age":      0,
	"Username": "",
	"Password": "123456",
})
// 等效於:UPDATE Users SET Age = ?, Password = ?, Username = ? WHERE Username = ?

刪除

刪除一筆資料再簡單不過了。

rushia.NewQuery("Users").Where("ID = ?", 1).Delete()
// 等效於:DELETE FROM Users WHERE ID = ?

選擇與取得

最基本的資料取得在 Rushia 中透過 Select 使用。

rushia.NewQuery("Users").Select()
// 等效於:SELECT * FROM Users

指定欄位

Select 中傳遞欄位名稱作為參數,多個欄位由逗點區分,亦能是函式。

rushia.NewQuery("Users").Select("Username", "Nickname")
// 等效於:SELECT Username, Nickname FROM Users

rushia.NewQuery("Users").Select(rushia.NewExpr("COUNT(*) AS Count"))
// 等效於:SELECT COUNT(*) AS Count FROM Users

單行資料

如果只想要取得單筆資料,那麼就可以用上 SelectOne,這簡單來說就是 .Limit(1).Select(...) 的縮寫。

rushia.NewQuery("Users").SelectOne("Username")
// 等效於:SELECT Username FROM Users LIMIT 1

排除重複

取得資料的時候可以指定 Distinct 過濾重複內容。

rushia.NewQuery("Products").Distinct().Select()
// 等效於:SELECT DISTINCT * FROM Products

聯集

可以透過 UnionUnionAll 整合多個表格選取之間的資料。

locationQuery := rushia.NewQuery("Locations").Select()

rushia.NewQuery("Users").Union(locationQuery).Select()
// 等效於:SELECT * FROM Users UNION SELECT * FROM Locations

rushia.NewQuery("Users").UnionAll(locationQuery).Select()
// 等效於:SELECT * FROM Users UNION ALL SELECT * FROM Locations

選擇是否存在

透過 Exists 來執行一個 SELECT EXISTS

rushia.NewQuery("Users").Where("Username = ?", "YamiOdymel").Exists()
// 等效於:SELECT EXISTS(SELECT * FROM Users WHERE Username = ?)

表格別名

As 能夠替目前的查詢語句賦予表格別名,通常會應用在子查詢。若是在表格加入(JOIN)或是一般場景,則可以使用 NewAlias

rushia.NewQuery(NewQuery("Users").Select()).As("Result").Where("Username = ?", "YamiOdymel").Select())
// 等效於:SELECT * FROM (SELECT * FROM Users) AS Result WHERE Username = ?

rushia.NewQuery(rushia.NewAlias("UserFriendRelationships", "relations")).Where("relations.ID = ?", 5).Select()
// 等效於: SELECT * FROM UserFriendRelationships AS relations relations.WHERE ID = ?

執行生指令

Rushia 已經提供了近乎日常中 80% 會用到的方式,但如果好死不死你想使用的功能在那 20% 之中,我們還提供了原生的方法能讓你直接輸入 SQL 指令執行自己想要的鳥東西。一個最基本的生指令(Raw Query)就像這樣。

其中亦能帶有預置聲明(Prepared Statement),也就是指令中的問號符號替代了原本的值。這能避免你的 SQL 指令遭受注入攻擊。

正如標準的 NewQuery 一樣,NewRawQuery 也需要透過 Build 才能建置。而且 NewRawQuery 不能使用所有輔助功能,如:LimitOrderBy…。

q := rushia.NewRawQuery("SELECT * FROM Users WHERE ID >= ?", 10)

條件宣告

透過 Rushia 宣告 WHEREHAVING 條件也能夠很輕鬆。這裡是實際應用最常派上用場的條件函式:

SQL 語法 使用方式
Column = ?
Column > ?
.Where("Column = ?", "Value")
.Where("Column > ?", "Value")
Column = Column .Where("Column = Column")
Column IN (?, ?)
Column NOT IN (?, ?)
.Where("Column IN (?, ?)", "A", "B")
.Where("Column NOT IN (?, ?)", "A", "B")
Column IN (?, ?) .Where("Column IN ?", []interface{}{"A", "B"})
Column BETWEEN ? AND ?
Column NOT BETWEEN ? AND ?
.Where("Column BETWEEN ? AND ?", 1, 20)
.Where("Column NOT BETWEEN ? AND ?", 1, 20)
Column IS NULL
Column IS NOT NULL
.Where("Column IS NULL")
.Where("Column IS NOT NULL")
Column EXISTS Query
Column NOT EXISTS Query
.Where("Column EXISTS ?", subQuery)
.Where("Column NOT EXISTS ?", subQuery)
Column LIKE ?
Column NOT LIKE ?
.Where("Column LIKE ?", "Value")
.Where("Column NOT LIKE ?", "Value")
(Column = Column OR Column = ?) .Where("(Column = Column OR Column = ?)", "Value")

這些函式總共有幾種變形,分別適用於 WhereOrWhereHavingOrHavingJoinWhereOrJoinWhere

rushia.NewQuery("Users").Where("ID = ?", 1).Where("Username = ?", "admin").Select()
// 等效於:SELECT * FROM Users WHERE ID = ? AND Username = ?

rushia.NewQuery("Users").Having("ID = ?", 1).Having("Username = ?", "admin").Select()
// 等效於:SELECT * FROM Users HAVING ID = ? AND Username = ?

rushia.NewQuery("Users").Where("ID != CompanyID").Where("DATE(CreatedAt) = DATE(LastLogin)").Select()
// 等效於:SELECT * FROM Users WHERE ID != CompanyID AND DATE(CreatedAt) = DATE(LastLogin)

預置聲明展開

透過預置聲明(Prepared Statement)可以避免 SQL 指令遭受注入攻擊。

在 Rushia 中有個額外的功能,傳入一個 Slice(無論是:[]interface{}[]int…等)給其中的單個 ? 會自動展開成為預置聲明。

rushia.NewQuery("Users").Where("ID IN ?", []interface{}{"A", "B", "C"}).Select()
// 等效於:SELECT * FROM Users WHERE ID IN (?, ?, ?)

脫逸值

mysqljs/mysql 套件中的 ?? 雙問號用法相同,你可以透過 ?? 產生出 (`) 來脫逸字元。這對於欄位名稱很有用。

var ColumnUserID = "ID"
rushia.NewQuery("Users").Where("?? = ?", ColumnUserID, 3).Select()
// 等效於:SELECT * FROM Users WHERE `ID` = ?

排序

Rushia 亦支援排序功能,如遞增或遞減,亦能擺放函式。

rushia.NewQuery("Users").OrderBy("ID ASC").OrderBy("Login DESC").OrderBy("RAND()").Select()
// 等效於:SELECT * FROM Users ORDER BY ID ASC, Login DESC, RAND()

從值排序

也能夠從值進行排序,只需要傳入一個切片即可。

rushia.NewQuery("Users").OrderByField("UserGroup", "SuperUser", "Admin", "Users").Select()
// 等效於:SELECT * FROM Users ORDER BY FIELD (UserGroup, ?, ?, ?) ASC

分組

簡單的透過 GroupBy 就能夠將資料由指定欄位分組。

rushia.NewQuery("Users").GroupBy("Name").Select()
// 等效於:SELECT * FROM Users GROUP BY Name

加入表格

Rushia 支援多種表格加入方式,如:InnerJoinLeftJoinRightJoinNaturalJoinCrossJoin。在 Join 時,最後一個參數預設可以擺入條件式。

rushia.
	NewQuery("Products").
	LeftJoin("Users", "Products.TenantID = Users.TenantID").
	Select("Users.Name", "Products.ProductName")
// 等效於:SELECT Users.Name, Products.ProductName FROM Products AS Products LEFT JOIN Users AS Users ON (Products.TenantID = Users.TenantID)

但你也可以將加入條件式拆開放到後面定義。

rushia.
	NewQuery("Products").
	LeftJoin("Users").
	JoinWhere("Products.TenantID = Users.TenantID")
	Select("Users.Name", "Products.ProductName")
// 等效於:SELECT Users.Name, Products.ProductName FROM Products AS Products LEFT JOIN Users AS Users ON (Products.TenantID = Users.TenantID)

條件限制

你亦能透過 JoinWhereOrJoinWhere 擴展表格加入的限制條件,使用時這個條件總是會加到最後一個 Join 的表格。

rushia.
	NewQuery("Products").
	LeftJoin("Users", "Products.TenantID = Users.TenantID").
	OrJoinWhere("Users.TenantID = ?", 5).
	Select("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 = ?)

子指令

Rushia 支援複雜的子指令,將一個指令語法帶入當成值使用就能夠將其當作子指令。

subQuery := rushia.NewQuery("VIPUsers").Select("UserID")

rushia.NewQuery("Users").Where("ID IN ?", subQuery).Select()
// 等效於:SELECT * FROM Users WHERE ID IN (SELECT UserID FROM VIPUsers)

插入

插入新資料時也可以使用子指令,但必須確保子指令只會回傳一個欄位與單行資料。

subQuery := rushia.NewQuery("Users").Where("ID = ?", 6).SelectOne("Name")

rushia.NewQuery("Products").Insert(rushia.H{
	"ProductName": "測試商品",
	"UserID":      subQuery,
	"LastUpdated": rushia.NewExpr("NOW()")
})
// 等效於:INSERT INTO Products (ProductName, UserID, LastUpdated) VALUES (?, (SELECT Name FROM Users WHERE ID = 6 LIMIT 1), NOW())

加入

就算是加入表格的時候也可以用上子指令,但你需要使用 As 為子指令建立別名。

subQuery := rushia.NewQuery("Users").As("Users").Where("Active = ?", 1).Select()

rushia.
	NewQuery("Products").
	LeftJoin(subQuery, "Products.UserID = Users.ID").
	Select("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 := rushia.NewQuery("Locations").Select()
rawQuery := rushia.NewRawQuery("SELECT UserID FROM Users WHERE EXISTS (?)", subQuery)

NewQuery("Products").WhereExists(rawQuery).Select()
// 等效於:SELECT * FROM Products WHERE EXISTS (SELECT UserID FROM Users WHERE EXISTS (SELECT * FROM Locations))

指令關鍵字

Rushia 也支援設置指令關鍵字。

rushia.NewQuery("Users").SetQueryOption("FOR UPDATE").Select()
// 等效於:SELECT * FROM Users FOR UPDATE

rushia.NewQuery("Users").SetQueryOption("SQL_NO_CACHE").Select()
// 等效於:SELECT SQL_NO_CACHE * FROM Users

rushia.NewQuery("Users").SetQueryOption("LOW_PRIORITY", "IGNORE").Insert(data)
// Gives: INSERT LOW_PRIORITY IGNORE INTO Users ...

複雜場景範例

jobHistories := rushia.NewQuery("JobHistories").
	Where("DepartmentID BETWEEN ? AND ?", 50, 100).
	Select("JobID")
jobs := rushia.NewQuery("Jobs").
	Where("JobID IN ?", jobHistories).
	GroupBy("JobID").
	Select("JobID", "AVG(MinSalary) AS MyAVG")
maxAverage := rushia.NewQuery(jobs).
	As("SS").
	Select("MAX(MyAVG)")
employees := rushia.NewQuery("Employees").
	GroupBy("JobID").
	Having("AVG(Salary) < ?", maxAverage).
	Select("JobID", "AVG(Salary)")

// 等效於:
// SELECT JobID,
//        AVG(Salary)
// FROM   Employees
// HAVING AVG(Salary) < (SELECT MAX(MyAVG)
//                       FROM   (SELECT JobID,
//                                      AVG(MinSalary) AS MyAVG
//                               FROM   Jobs
//                               WHERE  JobID IN (SELECT JobID
//                                                FROM   JobHistories
//                                                WHERE  DepartmentID BETWEEN 50
//                                                       AND 100
//                                               )
//                               GROUP  BY JobID) AS SS)
// GROUP  BY job_id;

agents := rushia.NewQuery("Agents").
	Where("Commission < ?", 0.12).
	Select()
customers := rushia.NewQuery("Customers").
	Where("Grade = ?", 3).
	Where("CustomerCountry <> ?", "India").
	Where("OpeningAmount < ?", 7000).
	Where("EXISTS ?", agents).
	Select("OutstandingAmount")
orders := rushia.NewQuery("Orders").
	Where("OrderAmount > ?", 2000).
	Where("OrderDate < ?", "01-SEP-08").
	Where("AdvanceAmount < ?", rushia.NewExpr("ANY (?)", customers)).
	Select("OrderNum", "OrderDate", "OrderAmount", "AdvanceAmount")

// 等效於:
// SELECT OrderNum,
//        OrderDate,
//        OrderAmount,
//        AdvanceAmount
// FROM   Orders
// WHERE  OrderAmount > 2000
//        AND OrderDate < '01-SEP-08'
//        AND AdvanceAmount < ANY (SELECT OutstandingAmount
//                                 FROM   Customers
//                                 WHERE  Grade = 3
//                                        AND CustomerCountry <> 'India'
//                                        AND OpeningAmount < 7000
//                                        AND EXISTS (SELECT *
//                                                    FROM   Agents
//                                                    WHERE  Commission < 0.12));

相關連結

這裡是 Rushia 受啟發,或是和資料庫有所關聯的連結。