iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 8
0
Modern Web

Go into Web!系列 第 8

Day8 | 使用 ORM 與資料庫進行互動

昨天我們使用 drivermysql 進行互動,但在使用的時候會發現,做任何動作都必須使用 raw SQL,這樣可能會有幾個問題產生

  • 在開發網頁時很容易被以 SQL Injection 的方式攻擊
  • 使用不同類型的資料庫全部語法都要改過

因此,我們今天會說明如何使用 ORM 來解決以上的問題。

什麼是 ORM

ORM 為 Object Relation Mapping 的縮寫,翻譯過來就是物件關聯對映

簡單來說,就是 將程式物件對應到資料庫內容的一種抽象化技術,為什麼要進行抽象化呢,主要的原因為當我們在進行開發時,主要專注在物件的描述即可,不用關注底層是使用何種資料庫,也就是在程式碼不變的情況下我們可以任意切換使用不同的資料庫

ORM 有著其他的優點,接下來會一一的介紹

大量減少重複的 SQL 語句

對於資料庫的操作最常使用的就是 CRUD,也就是所謂的 insert, select, updatedelete,然而如果在操作不同的 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 已經有基礎的理解了,接下來我們開始使用吧!

使用 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())
	}
}

執行後如果沒有任何錯誤訊息代表連線成功拉!

建立 Data Model

使用 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 詳細內容可以參考這裡

透過 Data Model 建立 Table

接著我們可以使用 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 本身有提供 FirstFind 方法,分別用於查詢一筆資料或是多筆資料,我們使用 First 進行測試,新增一個方法名為 FindUser,將 dbid 當作參數,回傳 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 方法,設定要查詢 id1

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 以及今天資料庫的相關設定,這樣的方式並不好,在未來程式如果要真正的上線,勢必會放在不同的環境中,不可能針對每一個環境都重新修改程式,因此,明天就讓我們來聊聊 配置檔的處理 吧!

參考


上一篇
Day7 | 使用 GoLang 與資料庫進行互動
下一篇
Day9 | 輕鬆管理程式的設定檔
系列文
Go into Web!30

尚未有邦友留言

立即登入留言