前提

平常在準備 LINE Bot 的相關範例程式碼的時候,通常都是沒有加上資料庫。但是有一些的範例程式碼其實有一些儲存資料會比較好。 所以這時候需要加上資料庫的相關讀寫。

當然…. 資料庫也是有窮人版的。由於許多服務都要對於他們的資料庫服務收費之後,這時候就需要有一些變通的方式。 使用記憶體當作資料庫的架設。

那如何讓你在程式碼中,只要寫一次關於資料處理方面,當你的部屬的環境變數有不同,就會使用不同的資料庫來存取呢?

比如說:

  • 當你有 PostGresSQL 的資料庫 URL ,就使用 PG SQL 相關處理方式。
  • 如果沒有的話,就使用記憶體做為資料庫。
  • 以後也可以增加不同的雲的部署方式。(或是支援 Firebase 相關資料庫)

這一篇文章將開始敘述,如何透過 Golang 的 Interfaces 的方式來達到類似繼承的效果。 使用同一份的程式邏輯程式碼,可以根據你設定的參數不同來讀取不同得資料庫。

範例程式碼 LINE Bot 群組聊天摘要生成器

這次透過上一次的範例文章 [學習文件] LINE Bot 群組聊天摘要生成器 作為一個範例程式碼。 先來看整體切割方式。

Github Repo: https://github.com/kkdai/LINE-Bot-ChatSummarizer

## 資料架構切割圖

image-20230113221237698

所有的 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)
}
}
view raw data_process.go hosted with ❤ by GitHub

這裡使用到定義成 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
view raw basic_data.go hosted with ❤ by GitHub

這是最基礎的設定,最重要記事 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),
}
}
view raw memory_db.go hosted with ❤ by GitHub

接下來這是使用 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
}
view raw pgsql_db.go hosted with ❤ by GitHub

接下來這邊就是使用 PostGresSQL 的實作,主要是透過 "github.com/go-pg/pg/v10" 這個套件的版本,可以透過 ORM 的方式直接去操作 PostgresSQL 可以讓許多實情省下麻煩。但是很多時候,沒有直接使用 SQL 其實也是更加的麻煩。

這邊的開發流程上,沒有要注意的事情。只需要注意到必須以下實作就好。

  • ReadGroupInfo(string) GroupData
  • AppendGroupInfo(string, MsgDetail)

img

未來發展

透過 Interfaces 來當作資料庫存取的開發方式可以很方便,並且留下未來許多資料庫的資源空間。不論是支援 MongoDB 或是想要使用 MySQL 甚至是整個資料庫搬到 FireStore 也不需要改動我原版的商業邏輯部分。 只需要把基本的資料庫實作完成即可。

希望這篇文章可以給大家一些想法。


Buy Me A Coffee

Evan

Attitude is everything