Go言語(golang)でTUIアプリを作ろう 第二部入魂編 ( その4 BoltManagerの拡充 )


それでは、続けてBoltManagerの機能を拡充していきます。


[1] Managerインタフェース(listdb/manager.go)

実装するメソッドは、Managerインタフェースに定義されている下記のメソッドになります。前回ConnectCloseDefineImportCSVは完了していますので、それ以外のメソッドを実装します。

type Manager interface {  
	Connect(databaseName string, connectString string) error  
	GetDbNames() ([]string, error)  
	GetCategoryList(dbName string) ([]string, error)  
	SearchDB(dbName string, category string, search string, from_rec int, count_rec int) *ListDB  
	GetRecordCount(dbName string, category string, search string) int  
	Insert(dbName string, listItem *ListItem) (int, error)  
	Update(dbName string, id int, listItem *ListItem) (*ListDB, error)  
	Delete(dbName string, id int) error  
	ImportCSV(fname string) bool  
	Define() error  
	Close()  
}  

[2] ListDB構造の変更(listdb/listdb.go)

その前に、データを格納するListDB部に、必要な構造体とメソッドを追加しておきます。

package listdb  
  
type ListDB struct {  
	ID            int  
	DbName        string  
	FieldName01   string  
	FieldName02   string  
	CategoryList  string  
	CurrentNumber int  
	ListData      []ListItem  
}  
  
type ListItem struct {  
	ID       int    `json:"id"`  
	Category string `json:"category"`  
	Field01  string `json:"field01"`  
	Field02  string `json:"field02"`  
	Note     string `json:"note"`  
}  
  
func (listdb *ListDB) AddListData(listItem ListItem) {  
	listdb.ListData = append(listdb.ListData, listItem)  
}  
  
func (listdb *ListDB) GetListData() []ListItem {  
	return listdb.ListData  
}  
  
func (listdb *ListDB) CountListData() int {  
	return len(listdb.ListData)  
}  
  

[3] BoltManagerの実装(listdb/boltmanager.go)

それでは、具体的に進めます。

(1) GetDbNames() ([]string, error)

Buckt”MetaTable”を全件読み、keyを取り出し、配列にapeendして返します。

func (self *BoltManager) GetDbNames() ([]string, error) {  
	var dbNames []string  
	err := self.GetDb().View(func(tx *bolt.Tx) error {  
		b := tx.Bucket([]byte(LISTDB)).Bucket([]byte(METATABLE))  
		err := b.ForEach(func(k, v []byte) error {  
			dbNames = append(dbNames, string(k))  
			return nil  
		})  
		return err  
	})  
	return dbNames, err  
}  

(2) GetCategoryList(dbName string) ([]string, error)

Buckt”MetaTable”をdbNameで読み、valueを取り出します。このデータはCSVで、Field01NameFieldName02categoryListの順となっているので、配列の3つ目以降を返します。

func (self *BoltManager) GetCategoryList(dbName string) ([]string, error) {  
	var categoryList []string  
	err := self.GetDb().View(func(tx *bolt.Tx) error {  
		b := tx.Bucket([]byte(LISTDB)).Bucket([]byte(METATABLE))  
		v := b.Get([]byte([]byte(dbName)))  
		s := strings.Split(string(v), ",")  
		categoryList = s[2:]  
		return nil  
	})  
	return categoryList, err  
}  

(3) SearchDB(dbName string, category string, search string, from_rec int, count_rec int) *ListDB

Boltでは、key部分を指定しての読み出しメソッドはいくつか提供されていますが、value部への検索処理はありません。しかたがないので、今回は極めて泥臭い手法で対応しました。Buckt”データ(dbName)”を順次読み、下記の条件に合うデータを抜き出します。

  • searchが含まれるデータ(指定されていた場合)
  • categoryが等しいデータ(指定されていた場合)
  • 開始レコード(from_rec)から、指定された件数(count_rec)分
func (self *BoltManager) contains(field string, search string) bool {  
	if search == "" {  
		return true  
	}  
	return strings.Contains(strings.ToLower(field), strings.ToLower(search))  
}  
  
func (self *BoltManager) SearchDB(dbName string, category string, search string, from_rec int, count_rec int) *ListDB {  
	var listdb *ListDB  
	_ = self.GetDb().View(func(tx *bolt.Tx) error {  
		b := tx.Bucket([]byte(LISTDB)).Bucket([]byte(METATABLE))  
		v := b.Get([]byte([]byte(dbName)))  
		s := strings.Split(string(v), ",")  
		listdb = new(ListDB)  
		listdb.DbName = dbName  
		listdb.FieldName01 = s[0]  
		listdb.FieldName02 = s[1]  
		listdb.CategoryList = strings.Join(s[2:], ",")  
  
		b = tx.Bucket([]byte(LISTDB)).Bucket([]byte(dbName))  
		c := b.Cursor()  
		var listItem ListItem  
		recCount := 1  
		targetCount := 1  
		for k, v := c.First(); k != nil; k, v = c.Next() {  
			if count_rec > 0 && targetCount > count_rec {  
				break  
			}  
			if err := json.Unmarshal(v, &listItem); err != nil {  
				return err  
			}  
  
			// check category  
			if category != "" && category != listItem.Category {  
				continue  
			}  
  
			// check string  
			if !self.contains(listItem.Field01, search) && !self.contains(listItem.Field02, search) && !self.contains(listItem.Note, search) {  
				continue  
			}  
  
			// add list within range  
			if recCount >= from_rec {  
				listdb.AddListData(listItem)  
				targetCount++  
			}  
			// count up  
			recCount++  
		}  
		return nil  
	})  
	return listdb  
}  

(4) GetRecordCount(dbName string, category string, search string) int

このメソッドもSearchDB同様、順次読み込みで対応しています。ソースは省略。

(5) Insert(dbName string, listItem *ListItem) (int, error)

以下の3つのメソッドは、Buckt”データ(dbName)”内のレコードを処理する物となります。特に説明は不要でしょう。

func (self *BoltManager) Insert(dbName string, listItem *ListItem) (int, error) {  
	var id int  
	err := self.GetDb().Update(func(tx *bolt.Tx) error {  
		b := tx.Bucket([]byte(LISTDB)).Bucket([]byte(dbName))  
		seq, _ := b.NextSequence()  
		id = int(seq)  
  
		buf, err := json.Marshal(listItem)  
		if err != nil {  
			return err  
		}  
		b.Put(self.itob(id), buf)  
		return nil  
	})  
	return id, err  
}  

(6) Update(dbName string, id int, listItem *ListItem) (*ListDB, error)

func (self *BoltManager) Update(dbName string, id int, listItem *ListItem) (*ListDB, error) {  
	err := self.GetDb().Update(func(tx *bolt.Tx) error {  
		b := tx.Bucket([]byte(LISTDB)).Bucket([]byte(dbName))  
		buf, err := json.Marshal(listItem)  
		if err != nil {  
			return err  
		}  
		b.Put(self.itob(id), buf)  
		return nil  
	})  
	return nil, err  
}  

(7) Delete(dbName string, id int) error

func (self *BoltManager) Delete(dbName string, id int) error {  
	err := self.GetDb().Update(func(tx *bolt.Tx) error {  
		b := tx.Bucket([]byte(LISTDB)).Bucket([]byte(dbName))  
		b.Delete(self.itob(id))  
		return nil  
	})  
	return err  
}  

[4] BoltManagerのテスト(cmd/testdb.go)

DBの内容を確認するプログラムも用意しました。まず、定義情報を読み出した後、いくつかの検索結果を表示しています。

$ go run cmd/testdb.go  
table:EQMM作品評価リスト(78号まで) categoryList:[1956 1957 1958 1959 1960 1961 1962]  
table:アンソロジー作品評価リスト categoryList:[「新青年傑作選」を読む 「日本代表ミステリー選集」を読む 「現代の推理小説」を読む 「探偵小説年鑑」を読む その他 「宝石傑作選」を読む 「ミステリーの愉しみ」を読む 現代推理小説体系を読む]  
table:ハードオフ店舗情報(東京都) categoryList:[練馬区 板橋区 千代田区 品川区 武 蔵野市 台東区 杉並区 世田谷区 多摩市 江戸川区 八王子市 立川市 東大和市 三鷹市 羽村市 小金井市 東村山市 小平市 稲城市 あきる野市 町田市 東久留米市 足立区 青梅市 江東区]  
table:ブックオフ店舗情報(東京都) categoryList:[千代田区 港区 新宿区 文京区 台東 区 墨田区 江東区 品川区 目黒区 大田区 世田谷区 渋谷区 中野区 杉並区 豊島区 北区 板橋区 練馬区 足立区 江戸川区 八王子市 立川市 武蔵野市 青梅市 府中市 昭島市 調布市 町田市 小平市 国立市 福生市 東大和市 東久留米市 多摩市 西東京市]  
------------------------------------------------------------------  
ID=0, DbName=EQMM作品評価リスト(78号まで), FieldName01=title, FieldName02=comment, CategoryList=1956,1957,1958,1959,1960,1961,1962  
1 | 1956 | 魔の森の家(カーター・ディクスン) | 9.0:三読目だろうが、これは名作です。伏線の張り方がすごいし、最後のHMのセリフも余韻を残す。クイーンの解説もよい。会話文が『』になっているのは訳者乱歩の指定らしい。どういう意図なのだろう。 | EQMM 1956/7 No.1 創刊号(早川書房)  
早川書房  
2018/07/31  
2 | 1956 | パーティーの夜(スタンリイ・エリン) | 5.0:今ひとつ展開に乏しい。 | EQMM 1956/7 No.1 創刊号(早川書房)  
早川書房  
2018/07/31  
3 | 1956 | 死者を鞭うつ勿れ(ジョン・コリア) | 7.0:ちょっとしたひねりが楽しい。 | EQMM 1956/7 No.1 創刊号(早川書房)  
早川書房  
2018/07/31  
Count=814  
                               :  
                               :  

以上で、BoltManagerの実装は完了です。

次回は、TUI部品にBoltMangerを組み込み、アプリケーションを完成させます。


ソースコードについて

GitHubに登録しました。今回のコードは、Section24となります。