昨天我們使用 driver
與 mysql
進行互動,但在使用的時候會發現,做任何動作都必須使用 raw SQL
,這樣可能會有幾個問題產生
SQL Injection
的方式攻擊因此,我們今天會說明如何使用 ORM
來解決以上的問題。
ORM 為 Object Relation Mapping
的縮寫,翻譯過來就是物件關聯對映。
簡單來說,就是 將程式物件對應到資料庫內容的一種抽象化技術
,為什麼要進行抽象化呢,主要的原因為當我們在進行開發時,主要專注在物件的描述即可,不用關注底層是使用何種資料庫,也就是在程式碼不變的情況下我們可以任意切換使用不同的資料庫。
而 ORM
有著其他的優點,接下來會一一的介紹
對於資料庫的操作最常使用的就是 CRUD,也就是所謂的 insert
, select
, update
與 delete
,然而如果在操作不同的 table 時就會重複的使用到這些語法,ORM 這些語法進行封裝,我們只要呼叫對應的方法就可以重複使用了。
直接使用 SQL 語句來操作資料庫,最常見的資安漏洞就是被使用 SQL Injection
的方式攻擊,SQL Injection
就是利用輸入的字串中夾帶 SQL 語句,導致資料庫在執行時被入侵或是破壞,以下為 SQL Injection
的情境
假設我們要查詢 users
這張 table,將 Id 當作是 Input,因此設計了一個 Select 語法如下
SELECT * FROM users WHERE Id = <id>;
一般而言就是輸入id,但透過 SQL Injection
的方式只需要在輸入的時候多加上 OR 1=1
就可以撈出所有的資料,用這種方式跳過驗證。
ORM
要避免 SQL Injection
的情況出現,多半會使用判斷輸入的字串是否有問題等方式處理,因此,使用 ORM 可以增加整體的安全性是毋庸置疑的。
使用 ORM 在開發時只要好好的面對物件即可,無需關注到原生的 SQL 需要怎麼下,在轉換不同類型的資料庫時也不用修改程式,可以大幅的降低程式與資料庫之間的耦合力。
講到這裡,相信大家對於 ORM 已經有基礎的理解了,接下來我們開始使用吧!
目前 golang 有需多開源的 ORM 可以使用,詳細的部分可以參考這裡。
我自己是選用 gorm
,主要是因為他的文件非常完整與可以支援熱加載(Eager loading),還可以進行 Auto Migrations
,接下來就讓我們開始使用 gorm
吧
使用 gorm 非常簡單,只要透過 go get
的方法進行安裝,同時也要安裝由 gorm 官方所提供的 driver
,這邊以 mysql
為例
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
在使用以前要先進行 import
,匯入 gorm 與 mysql driver
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
接著我們將連線的資訊設定為常數
const (
USERNAME = "demo1"
PASSWORD = "demo123"
NETWORK = "tcp"
SERVER = "127.0.0.1"
PORT = 3306
DATABASE = "demo"
)
透過 gorm
連線到資料庫與昨天講的 driver 非常相似,都是透過 dsn
進行連線
dsn := fmt.Sprintf("%s:%s@%s(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",USERNAME,PASSWORD,NETWORK,SERVER,PORT,DATABASE)
_, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
檢查資料庫是否連線正常
panic("使用 gorm 連線 DB 發生錯誤,原因為 " + err.Error())
}
整體的連線程式會像這樣
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
const (
USERNAME = "demo"
PASSWORD = "demo123"
NETWORK = "tcp"
SERVER = "127.0.0.1"
PORT = 3306
DATABASE = "demo"
)
func main() {
dsn := fmt.Sprintf("%s:%s@%s(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",USERNAME,PASSWORD,NETWORK,SERVER,PORT,DATABASE)
_, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("使用 gorm 連線 DB 發生錯誤,原因為 " + err.Error())
}
}
執行後如果沒有任何錯誤訊息代表連線成功拉!
使用 ORM 顧名思義就是要使用 Object,因此我們先來設定一個名為 user 的 struct
type User struct {
ID int64 `json:"id" gorm:"primary_key;auto_increase'"`
Username string `json:"username"`
Password string `json:""`
}
要將 struct mapping 到資料庫的內容,必須要使用 gorm
的 tag,tag 詳細內容可以參考這裡。
接著我們可以使用 gorm 內建的 AutoMigrate
方法進行 Table 的建立,很多人一定想問,為什麼要使用 AutoMigrate 方法呢,因為使用這個方法就可以在沒有 Table 時新增,Table 有更動時自動更新,因此選用這個方法
if err := db.AutoMigrate(new(User)); err != nil {
panic("資料庫 Migrate 失敗,原因為 " + err.Error())
}
我們可以將整體修改為這樣
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
const (
USERNAME = "demo"
PASSWORD = "demo123"
NETWORK = "tcp"
SERVER = "127.0.0.1"
PORT = 3306
DATABASE = "demo"
)
type User struct {
ID int64 `json:"id" gorm:"primary_key;auto_increase'"`
Username string `json:"username"`
Password string `json:""`
}
func main() {
dsn := fmt.Sprintf("%s:%s@%s(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", USERNAME, PASSWORD, NETWORK, SERVER, PORT, DATABASE)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("使用 gorm 連線 DB 發生錯誤,原因為 " + err.Error())
}
if err := db.AutoMigrate(new(User)); err != nil {
panic("資料庫 Migrate 失敗,原因為 " + err.Error())
}
}
執行出來的結果如果沒有跳出錯誤訊息代表成功的建立 Table 了,我們可以透過任何資料庫管理工具查看結果是否正確。
新增資料變得非常的簡單,只要建立一個新的物件後,呼叫 gorm 所提供的 Create
方法即可,我們建立一個名為 CreateUser
的方法
func CreateUser(db *gorm.DB, user *User) error {
return db.Create(user).Error
}
之後修改 main
方法,建立一個 User
物件後呼叫此方法
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
const (
USERNAME = "demo"
PASSWORD = "demo123"
NETWORK = "tcp"
SERVER = "127.0.0.1"
PORT = 3306
DATABASE = "demo"
)
type User struct {
ID int64 `json:"id" gorm:"primary_key;auto_increase'"`
Username string `json:"username"`
Password string `json:""`
}
func main() {
dsn := fmt.Sprintf("%s:%s@%s(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", USERNAME, PASSWORD, NETWORK, SERVER, PORT, DATABASE)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("使用 gorm 連線 DB 發生錯誤,原因為 " + err.Error())
}
if err := db.AutoMigrate(new(User)); err != nil {
panic("資料庫 Migrate 失敗,原因為 " + err.Error())
}
user := &User{
Username: "test",
Password: "test",
}
if err := CreateUser(db, user)); err != nil {
panic("資料庫 Migrate 失敗,原因為 " + err.Error())
}
}
func CreateUser(db *gorm.DB, user *User) error {
return db.Create(user).Error
}
執行出來的結果如果沒有跳出錯誤訊息代表成功的建立 User 了,我們可以透過任何資料庫管理工具查看結果是否正確。
查詢的部分更為簡單,gorm
本身有提供 First
與 Find
方法,分別用於查詢一筆資料或是多筆資料,我們使用 First
進行測試,新增一個方法名為 FindUser
,將 db
與 id
當作參數,回傳 User
func FindUser(db *gorm.DB, id int64) (*User, error) {
user := new(User)
user.ID = id
err := db.First(&user).Error
return user, err
}
之後修改 main
方法,設定要查詢 id
為 1
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"log"
)
const (
USERNAME = "demo"
PASSWORD = "demo123"
NETWORK = "tcp"
SERVER = "127.0.0.1"
PORT = 3306
DATABASE = "demo"
)
type User struct {
ID int64 `json:"id" gorm:"primary_key;auto_increase'"`
Username string `json:"username"`
Password string `json:""`
}
func main() {
dsn := fmt.Sprintf("%s:%s@%s(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", USERNAME, PASSWORD, NETWORK, SERVER, PORT, DATABASE)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("使用 gorm 連線 DB 發生錯誤,原因為 " + err.Error())
}
if err := db.AutoMigrate(new(User)); err != nil {
panic("資料庫 Migrate 失敗,原因為 " + err.Error())
}
user := &User{
Username: "test",
Password: "test",
}
if err := CreateUser(db, user);err != nil {
panic("新增 user 失敗,原因為 " + err.Error())
}
if user, err := FindUser(db, 1); err == nil {
log.Println("查詢到 User 為 ", user)
} else {
panic("查詢 user 失敗,原因為 " + err.Error())
}
}
func CreateUser(db *gorm.DB, user *User) error {
return db.Create(user).Error
}
func FindUser(db *gorm.DB, id int64) (*User, error) {
user := new(User)
user.ID = id
err := db.First(&user).Error
return user, err
}
執行出來的結果如果顯示像 2020/09/08 23:59:42 查詢到 User 為 &{1 test test}
代表成功查詢了,我們可以透過任何資料庫管理工具查看結果是否正確。
更多語法可以參考 gorm
的官方文件。
從今天的範例可以看出,使用 orm 可以讓我們的程式更為簡潔,而且單純使用物件的方式就可以與資料庫進行互動,讓我們有更多的精神 focus 在產品的開發上。
在這幾天的文章中可以發現,許多的設定我們都把他寫在程式內,像是前幾天講到的建立網站的 port 以及今天資料庫的相關設定,這樣的方式並不好,在未來程式如果要真正的上線,勢必會放在不同的環境中,不可能針對每一個環境都重新修改程式,因此,明天就讓我們來聊聊 配置檔的處理
吧!