前提
平常在準備 LINE Bot 的相關範例程式碼的時候,通常都是沒有加上資料庫。但是有一些的範例程式碼其實有一些儲存資料會比較好。 所以這時候需要加上資料庫的相關讀寫。
當然…. 資料庫也是有窮人版的。由於許多服務都要對於他們的資料庫服務收費之後,這時候就需要有一些變通的方式。 使用記憶體當作資料庫的架設。
那如何讓你在程式碼中,只要寫一次關於資料處理方面,當你的部屬的環境變數有不同,就會使用不同的資料庫來存取呢?
比如說:
- 當你有 PostGresSQL 的資料庫 URL ,就使用 PG SQL 相關處理方式。
- 如果沒有的話,就使用記憶體做為資料庫。
- 以後也可以增加不同的雲的部署方式。(或是支援 Firebase 相關資料庫)
這一篇文章將開始敘述,如何透過 Golang 的 Interfaces 的方式來達到類似繼承的效果。 使用同一份的程式邏輯程式碼,可以根據你設定的參數不同來讀取不同得資料庫。
範例程式碼 LINE Bot 群組聊天摘要生成器
這次透過上一次的範例文章 [學習文件] LINE Bot 群組聊天摘要生成器 作為一個範例程式碼。 先來看整體切割方式。
Github Repo: https://github.com/kkdai/LINE-Bot-ChatSummarizer
## 資料架構切割圖
所有的 implement 都是透過 Data 也就是之後 Basic Class 的 API 來存取相關資料。 只要建立的時候,使用相關的 Interfaces 搭配不同的起始變數就可以呼叫同樣的處理資訊。
先列出相關的處理程式碼:
相關處理程式碼
// 如果有預設 DABTASE_URL 就建立 PostGresSQL; 反之則建立 Mem DB | |
pSQL := os.Getenv("DATABASE_URL") | |
if pSQL != "" { | |
summaryQueue = NewPGSql(pSQL) | |
} else { | |
summaryQueue = NewMemDB() | |
} | |
//.... | |
func handleSumAll(event *linebot.Event, message string) { | |
// 把聊天群組裡面的訊息都捲出來(依照先後順序) | |
oriContext := "" | |
q := summaryQueue.ReadGroupInfo(getGroupID(event)) | |
for _, m := range q { | |
// [xxx]: 他講了什麼... 時間 | |
oriContext = oriContext + fmt.Sprintf("[%s]: %s . %s\n", m.UserName, m.MsgText, m.Time.Local().UTC().Format("2006-01-02 15:04:05")) | |
} | |
... | |
} | |
func handleListAll(event *linebot.Event, message string) { | |
reply := "" | |
q := summaryQueue.ReadGroupInfo(getGroupID(event)) | |
for _, m := range q { | |
reply = reply + fmt.Sprintf("[%s]: %s . %s\n", m.UserName, m.MsgText, m.Time.Local().UTC().Format("2006-01-02 15:04:05")) | |
} | |
if _, err := bot.ReplyMessage(event.ReplyToken, linebot.NewTextMessage(reply)).Do(); err != nil { | |
log.Print(err) | |
} | |
} | |
這裡使用到定義成 Interfaces 的 GroupDB
的實作,根據不同的設定 NewPGSql(url)
或是 NewMemDB()
就可以讓裡面對應的實作不同。
詳細列出不同資料庫的開發方式
接下來列出不同資料庫的實作方式。
Basic (Data)
type GroupDB interface { | |
ReadGroupInfo(string) GroupData | |
AppendGroupInfo(string, MsgDetail) | |
} | |
type MsgDetail struct { | |
MsgText string | |
UserName string | |
Time time.Time | |
} | |
type GroupData []MsgDetail |
這是最基礎的設定,最重要記事 interface GroupDB
的宣告,然後其他兩個也必須要有
ReadGroupInfo(string) GroupData
AppendGroupInfo(string, MsgDetail)
兩個 function 的實作,並且輸入參數跟輸出參數都要相同。 這樣才能使用到一樣的邏輯來操作資料。
Memory DB
type MemStorage map[string]GroupData | |
type MemDB struct { | |
db MemStorage | |
} | |
func (mdb *MemDB) ReadGroupInfo(roomID string) GroupData { | |
return mdb.db[roomID] | |
} | |
func (mdb *MemDB) AppendGroupInfo(roomID string, m MsgDetail) { | |
mdb.db[roomID] = append(mdb.db[roomID], m) | |
} | |
func NewMemDB() *MemDB { | |
return &MemDB{ | |
db: make(MemStorage), | |
} | |
} |
接下來這是使用 Memory 做為資料庫的實作,可以看到主要是透過 map
來操作相關資料處理。 這樣透過 memory 當作 DB 的方式,如果是在 FAAS (e.g. Heroku 或是 Render.com) 就會在服務睡眠的時候,失去你的儲存資料。
PostGresSQL DB
type PGSqlDB struct { | |
Db *pg.DB | |
} | |
func (mdb *PGSqlDB) ReadGroupInfo(roomID string) GroupData { | |
pgsql := &DBStorage{ | |
RoomID: roomID, | |
} | |
if ret, err := pgsql.Get(mdb); err == nil { | |
return ret.Dataset | |
} else { | |
log.Println("DB read err:", err) | |
} | |
return GroupData{} | |
} | |
func (mdb *PGSqlDB) AppendGroupInfo(roomID string, m MsgDetail) { | |
u := mdb.ReadGroupInfo(roomID) | |
u = append(u, m) | |
pgsql := &DBStorage{ | |
RoomID: roomID, | |
} | |
if err := pgsql.Update(mdb); err != nil { | |
log.Println("DB update err:", err) | |
} | |
} | |
func NewPGSql(url string) *PGSqlDB { | |
options, _ := pg.ParseURL(url) | |
db := pg.Connect(options) | |
err := createSchema(db) | |
if err != nil { | |
panic(err) | |
} | |
return &PGSqlDB{ | |
Db: db, | |
} | |
} | |
func createSchema(db *pg.DB) error { | |
models := []interface{}{ | |
(*MemStorage)(nil), | |
} | |
for _, model := range models { | |
err := db.Model(model).CreateTable(&orm.CreateTableOptions{ | |
IfNotExists: true}) | |
if err != nil { | |
return err | |
} | |
} | |
return nil | |
} | |
// DBStorage: for orm db storage. | |
type DBStorage struct { | |
Id int64 `bson:"_id"` | |
RoomID string `json:"roomid" bson:"roomid"` | |
Dataset GroupData `json:"dataset" bson:"dataset"` | |
} | |
func (u *DBStorage) Add(conn *PGSqlDB) { | |
_, err := conn.Db.Model(u).Insert() | |
if err != nil { | |
log.Println(err) | |
} | |
} | |
func (u *DBStorage) Get(conn *PGSqlDB) (result *DBStorage, err error) { | |
log.Println("***Get dataset roomID=", u.RoomID) | |
data := DBStorage{} | |
err = conn.Db.Model(&data). | |
Where("Room ID = ?", u.RoomID). | |
Select() | |
if err != nil { | |
log.Println(err) | |
return nil, err | |
} | |
log.Println("DB result= ", data) | |
return &data, nil | |
} | |
func (u *DBStorage) Update(conn *PGSqlDB) (err error) { | |
log.Println("***Update DB group data=", u) | |
_, err = conn.Db.Model(u). | |
Set("dataset = ?", u.Dataset). | |
Where("roomid = ?", u.RoomID). | |
Update() | |
if err != nil { | |
log.Println(err) | |
} | |
return nil | |
} |
接下來這邊就是使用 PostGresSQL 的實作,主要是透過 "github.com/go-pg/pg/v10"
這個套件的版本,可以透過 ORM 的方式直接去操作 PostgresSQL 可以讓許多實情省下麻煩。但是很多時候,沒有直接使用 SQL 其實也是更加的麻煩。
這邊的開發流程上,沒有要注意的事情。只需要注意到必須以下實作就好。
ReadGroupInfo(string) GroupData
AppendGroupInfo(string, MsgDetail)
未來發展
透過 Interfaces 來當作資料庫存取的開發方式可以很方便,並且留下未來許多資料庫的資源空間。不論是支援 MongoDB 或是想要使用 MySQL 甚至是整個資料庫搬到 FireStore 也不需要改動我原版的商業邏輯部分。 只需要把基本的資料庫實作完成即可。
希望這篇文章可以給大家一些想法。