Go言語(golang)でTUIアプリを作ろう 第二部入魂編 ( その3 DB設計とデータのロード )
BoltDB上にListDBを定義し、CSVからデータを追加してみる。
[1] ListDBとBoltの構造
今回は、Bolt上にListDBを定義し、実際にデータをロードしていきます。ListDBの構造については、PythonでTUIアプリを作ろう 第二部入魂編(その2 ListDBの構造について)を参照してください。
(1) 基本構造
まずは、Bolt上のBucketを定義しましょう。
Root bucketは、”ListDB"という名称とし、その下のNested bucketsとして、定義情報(”MetaTbale”)と実際のデータ(以下、そのデータ名をdbNameとする)を定義します。Bucketのkeyとvalueは下記とします。
Bucket ”MetaTable” Key:dbName Value:CSV(fieldName01,fieldName02,categoryList)
Bucket dbName Key:自動連番 Value:JSON(category,field01,field02,note)
概念図を下記に示します。ListDBとBoltは、Managerと呼ばれるインタフェースを継承したBoltManagerでマッピングされることとなります。
[2] データロード用CSVファイル
今回使用するデータは、PythonでTUIアプリを作ろう 第二部入魂編(その3 スクレイピングによるデータ収集)で作成したものを流用します。以下、データとプログラムは、ここからダウンロードし、参照ください。
(1) CSVの形式
CSVは、1件目が定義情報、2件目以降が実データになります。
1) 定義情報
データ名,フィールド1の名称,フィールド2の名称,CategoryList
の様に指定します。CategoryListは、カンマ(’,’)で複数設定しておきます。
2) 実データ
Category,Field01,Field02,Note
上記の様に指定します。Note内のカンマ(’,’)は、’改行’に変換されて、複数行データとなります。
以下に、実際のCSVファイル(csv/bookoff_tokyo.csv)を示します。
ブックオフ店舗情報(東京都),name,address,千代田区,港区,新宿区,文京区,台東区,墨田区,江東区,品川区,目黒区,大田区,世田谷区,渋谷区,中野区,杉並区,豊島区,北区,板橋区,練馬区,足立区,江戸川区,八王子市,立川市,武蔵野市,青梅市,府中市,昭島市,調布市,町田市,小平市,国立市,福生市,東大和市,東久留米市,多摩市,西東京市
千代田区,BOOKOFF 秋葉原駅前店,東京都千代田区神田佐久間町1-6-4 ,03-5207-6206,10:00~21:00
港区,BOOKOFF総合買取窓口 田町駅西口店,東京都港区芝5丁目32-3 1F,03-5439-4131,11:00~20:00
新宿区,BOOKOFF 飯田橋駅東口店,東京都新宿区揚場町1-11 ,03-5206-6831,10:00~21:00
:
:
この例では、
dbName : ブックオフ店舗情報(東京都)
フィールド1の名称 : name
フィールド2の名称 : address
CategoryList : 千代田区,港区,新宿区,....
として定義しています。
Bolt上では、Bucket”MetaTable"にこの定義情報を格納、2件目以降はdbNameをIDとしたBucketに順次格納することになります。
[3] Managerインタフェースの構成
まずは、データロードを司るManager(listdb/manager.go)を実装します。ここでは、メソッドの定義と、実Manager(BoltManager)の生成を行います。
package listdb
// ----------------------------------------------------------------------
type Manager interface {
Connect(databaseName string, connectString string) error
ImportCSV(fname string) bool
Define() error
Close()
}
func GetManager(name string) Manager {
if name == "BOLT" {
return new(BoltManager)
} else {
//return new(ListDBManager)
}
return nil
}
[4] BoltManagerの実装(listdb/boltmanager.go)
次に、実際にデータロード処理を行うBoltManagerを実装します。このプログラムは長くなるので、Bucketの定義とデータの追加部分のみ説明していきます。
(1) Bucket(MetaTable)の定義
まずは、下記のコードでrootとなるBucket”ListDB"と、Nested bucketの”MetaTable"を、read-write transaction内で作成します。
func (self *BoltManager) Define() error {
err := self.GetDb().Update(func(tx *bolt.Tx) error {
tx.DeleteBucket([]byte(LISTDB))
root, err := tx.CreateBucketIfNotExists([]byte(LISTDB))
if err != nil {
return fmt.Errorf("D.ER could not create root bucket: %v", err)
}
root.DeleteBucket([]byte(METATABLE))
_, err = root.CreateBucketIfNotExists([]byte(METATABLE))
if err != nil {
return fmt.Errorf("D.ER could not create weight bucket: %v", err)
}
return nil
})
if err != nil {
return fmt.Errorf("D.ER could not set up buckets, %v", err)
}
return nil
}
(2) Bucket(MetaTable)へのデータ追加
Putメソッドを利用し、データをBucketに格納します。MetaTableには、CSVをそのままvalueとして追加しています。
func (self *BoltManager) setMetaTable(tx *bolt.Tx, fields []string) error {
:
:
err := tx.Bucket([]byte(LISTDB)).Bucket([]byte(METATABLE)).Put([]byte(fields[0]), []byte(strings.Join(fields[1:point], ",")))
if err != nil {
return fmt.Errorf("D.ER could not set config: %v", err)
}
return nil
}
(3) Bucket(データ名)の定義
Buckt”MetaTable”と同様、rootとなるBucket”ListDB"にNested bucketとしてデータ部分(dbName)を定義します。
func (self *BoltManager) defineList(tx *bolt.Tx, dbName string) error {
root := tx.Bucket([]byte(LISTDB))
_, err := root.CreateBucketIfNotExists([]byte(dbName))
if err != nil {
return fmt.Errorf("D.ER could not create root bucket: %v", err)
}
return nil
}
(4) Bucket(データ名)へのデータ追加
Metatable同様、Putメソッドを利用し、データをBucketに格納します。ここでは、構造にセットしたデータをJSONに変換しています。
func (self *BoltManager) itob(v int) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(v))
return b
}
func (self *BoltManager) addList(tx *bolt.Tx, dbName string, fields []string) error {
listItem := new(ListItem)
b := tx.Bucket([]byte(LISTDB)).Bucket([]byte(dbName))
id, _ := b.NextSequence()
listItem.ID = int(id)
listItem.Category = fields[0]
listItem.Field01 = fields[1]
listItem.Field02 = fields[2]
listItem.Note = strings.Join(fields[3:], "\n")
buf, err := json.Marshal(listItem)
if err != nil {
return err
}
b.Put(self.itob(listItem.ID), buf)
return nil
}
golangでは、構造体に下記のようなタグをつけることで、簡単にJSONへマッピング(Marshal、Unmarshal)できますので、変換後BucketにPutしています。
type ListItem struct {
ID int `json:"id"`
Category string `json:"category"`
Field01 string `json:"field01"`
Field02 string `json:"field02"`
Note string `json:"note"`
}
(5) CSVの読み込みとデータロード
上記(1)から(4)のメソッドを使用して、CSVファイルをBucketにロードします。ここでのtransactionは、Begin、Commitメソッドを使用し手動設定しています。
func (self *BoltManager) ImportCSV(fname string) bool {
var fp *os.File
var err error
fp, err = os.Open(fname)
if err != nil {
return false
}
defer fp.Close()
reader := csv.NewReader(fp)
reader.Comma = ','
reader.LazyQuotes = true
reader.FieldsPerRecord = -1 // Nocheck fields count
var firstTime = true
var dbName string
tx, err := self.GetDb().Begin(true)
if err != nil {
return false
}
defer tx.Rollback()
for {
fields, err := reader.Read()
if err == io.EOF {
break
} else if err != nil {
fmt.Println(err)
return false
}
if len(fields) == 0 {
continue
}
if firstTime == true {
err = self.setMetaTable(tx, fields)
if err != nil {
return false
}
dbName = fields[0]
err = self.defineList(tx, dbName)
if err != nil {
return false
}
firstTime = false
} else {
err = self.addList(tx, dbName, fields)
if err != nil {
return false
}
}
}
if err = tx.Commit(); err != nil {
return false
}
return true
}
[5] ロードプログラム(cmd/loaddb.go)
実際にデータロードを実行するプログラムです。ディレクトリ内のCSVファイル(.csv)全件を対象に、一括ロードできるように設定しておきます。これにより、CSVファイルが増加しても、ディレクトリにファイルを追加するだけで、もれなく処理することができるわけです。
package main
import (
"fmt"
"io/ioutil"
"listdbg/listdb"
"path/filepath"
"strings"
"time"
)
func GetFilesFromDir(dir string) []string {
files, err := ioutil.ReadDir(dir)
if err != nil {
panic(err)
}
var paths []string
for _, file := range files {
if file.IsDir() {
paths = append(paths, GetFilesFromDir(filepath.Join(dir, file.Name()))...)
continue
}
paths = append(paths, filepath.Join(dir, file.Name()))
}
return paths
}
func loadDB(manager listdb.Manager, databaseName string, connectString string, csvdir string) {
var retCode bool
var err error
start := time.Now()
err = manager.Connect(databaseName, connectString)
if err != nil {
panic(err)
}
if err = manager.Define(); err != nil {
panic(err)
}
csvfiles := GetFilesFromDir(csvdir)
for _, csvfile := range csvfiles {
pos := strings.LastIndex(csvfile, ".")
if csvfile[pos:] == ".csv" {
retCode = manager.ImportCSV(csvfile)
fmt.Printf("FileName:%s, RetCode:%t\n", csvfile, retCode)
}
}
manager.Close()
end := time.Now()
fmt.Printf("%fsec\n", (end.Sub(start)).Seconds())
}
func main() {
var manager = listdb.GetManager("BOLT")
loadDB(manager, "BOLT", "./db/ListDB.boltdb", "./csv")
}
[6] プログラムの構造と実行
プログラムのディレクトリ構造は下記のようになります。
cmd/
loaddb.go
csv/
anthology.csv
bookoff_tokyo.csv
eqmm.csv
hardoff_tokyo.csv
db/
ListDB.boltdb
listdb/
boltmanager.go
listdb.go
manager.go
go.mod
go.sum
実行は下記のとおりです。
$ go run cmd/loaddb.go
FileName:csv/anthology.csv, RetCode:true
FileName:csv/bookoff_tokyo.csv, RetCode:true
FileName:csv/eqmm.csv, RetCode:true
FileName:csv/hardoff_tokyo.csv, RetCode:true
次回は、BoltManagerの機能を拡充していきます。