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とする)を定義します。Bucketkeyvalueは下記とします。

Bucket ”MetaTable” Key:dbName  Value:CSV(fieldName01,fieldName02,categoryList)  
Bucket  dbName    Key:自動連番 Value:JSON(category,field01,field02,note)  

概念図を下記に示します。ListDBBoltは、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件目以降はdbNameIDとした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)できますので、変換後BucketPutしています。

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は、BeginCommitメソッドを使用し手動設定しています。

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の機能を拡充していきます。


ソースコードについて


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