LINE Corporation 於2023年10月1日成爲 LY Corporation。LY Corporation 的新部落格在這裏。LY Corporation Tech Blog

Blog


如何透過 Golang 的 Interfaces 達成繼承 (inheritance) 的效果 - 以 LINE Bot 連接不同資料庫為例子

前提

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

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

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

比如說:

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

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

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

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

img

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

資料架構切割圖

所有的 implement 都是透過 Data 也就是之後 Basic Class 的 API 來存取相關資料。 只要建立的時候,使用相關的 Interfaces 搭配不同的起始變數就可以呼叫同樣的處理資訊。

先列出相關的處理程式碼:

相關處理程式碼

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)

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 也不需要改動我原版的商業邏輯部分。 只需要把基本的資料庫實作完成即可。

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