SQLite3で指定できるデータ型

Myfare cardを追加してテーブル作成しようとしたら、特定のカードのテーブルの値がstring指定したはずなのに”Inf”になってしまった。Infとはおそらく無限大ということだろうからstring型を指定しても機能しないらしい。

で調べるとSQLiteで指定できる型にはstringは無い。

https://www.javadrive.jp/sqlite/type/index1.html

おそらくstringを指定しても整数扱いとなってオーバーフローしてしまったらしい。

string -> text

int -> integer

に変更して作り直してうまくいっているようです。GORMで指定する構造体ではこのように変更しようがない(おそらくGORMが自動で変換する)から、テーブル作成時のSQLコマンドだけでの対応。

 

admin

Golangでwebsocketの実装

websocket自体はRFCで規定されているものですが、実装はそれぞれの言語ごとに存在します。

サンプルは、

https://zenn.dev/empenguin/articles/bcf95c19451020

元を辿ると、おそらくこちら。

https://echo.labstack.com/cookbook/websocket/

他の言語の例に漏れず、クライアント側のJavaScriptとサーバー側のGoのスクリプトが連携して動作します。ディレクトリ構成は以下の通りです。

<main.goのリクエスト処理部分>

func handleWebSocket(c echo.Context) error {
	websocket.Handler(func(ws *websocket.Conn) {
		defer ws.Close()

		// 初回のメッセージを送信
		err := websocket.Message.Send(ws, "Server: Hello, Client!")
		if err != nil {
			c.Logger().Error(err)
		}

		for {
			// Client からのメッセージを読み込む
			msg := ""
			err = websocket.Message.Receive(ws, &msg)
			if err != nil {
				//c.Logger().Error(err)
			}

			// Client からのメッセージを元に返すメッセージを作成し送信する
			err := websocket.Message.Send(ws, fmt.Sprintf("Server: \"%s\" received!", msg))
			if err != nil {
				//c.Logger().Error(err)
			}
		}
	}).ServeHTTP(c.Response(), c.Request())
	return nil
}

最初のページリクエスト後はwebsocketのループに入るので、ここでページのリロード(複数回ルートのリクエスト)をするとループ処理(for loop)でエラーが発生します。

一点疑問はwebsocketのポート番号指定をどこでやっているのかわからないこと。

P.S. 2023/4/15

ポートはHTTPと同じポートが使われています。WSはHTTPとは異なるプロトコルなので同じポートでも問題はないようですね。

 

admin

Golangのテンプレートエンジン(html/template)

動的なページ作成のためにはテンプレートエンジンが必須で、Node.jsの場合にはexpress-generatorに同様の機能が存在しますが、golangでもライブラリhtml/templateを使うと実現できます。

https://isehara-3lv.sakura.ne.jp/blog/2023/03/29/golangとnode-jsでmifareカード使ってチェックイン表示/

ではビューはNode.jsを使っていますが、ラズパイで動かすと実行速度あるいは実行ファイルの扱いやすさなどを考えるとGolangに統一した方が良いだろうからhtml/templateを使います。myfareカードから読み取りデータを受信するGoのスクリプトはほぼそのままです。

https://www.twihike.dev/docs/golang-web/templates

上記を参考に作成してみました。

<web server起動のスクリプト>

myfareカードを読み取ってDBをアップデートする関数はgoオプションで並行処理として起動(go uidSerial.SerialMain())。

package main

import (
	"fmt"

	"html/template"
	"myfare/uidSerial"
	"net/http"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"strconv"
	"time"
)

type Ninjya struct {
	Uid  string `gorm:"primaryKey"`
	Name string
	Time int
	Stat int
}

var products []Ninjya
var ninjyaSlice []string

// implements TableName of the Tabler interface
func (Ninjya) TableName() string {
	t := time.Now()
	tableName := "tbl" + strconv.Itoa(t.Year()) + strconv.Itoa(t.YearDay())
	return tableName
}

// to get active ninjya slice
func ninjya() {
	db, err := gorm.Open(sqlite.Open("./myfare.db"), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}

	// Migrate the schema
	db.AutoMigrate(&Ninjya{}) // if you use Automigrate and change struct, it won't be reflected automatically

	db.Debug().Order("Time desc").Where("Stat = ?", 1).Find(&products) // SELECT * FROM where Stat = tbl*****;
	ninjyaSlice = nil
	for i, p := range products {
		ninjyaSlice = append(ninjyaSlice, p.Name)
		fmt.Println(i, p)
	}
}

func handler(w http.ResponseWriter, r *http.Request) {
	t := template.Must(template.ParseFiles("layout.html", "pageData.html"))
	// to update ninjya status
	ninjya()
	tm := time.Now().Format(time.RFC1123)
	err := t.Execute(w, map[string]interface{}{
		"Time": tm,
		"Slice": ninjyaSlice,
	})
	if err != nil {
		fmt.Fprintln(w, err)
	}
}

func wevServer() {
	mux := http.NewServeMux()
	// to include static resoureces
	mux.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/"))))
	mux.HandleFunc("/", handler)
	server := http.Server{
		Addr:    ":8080",
		Handler: mux,
	}
	err := server.ListenAndServe()
	if err != nil {
		if err != http.ErrServerClosed {
			panic(err)
		}
	}
}

func main() {
	// to call card reader function()
	go uidSerial.SerialMain()

	// http server start
	wevServer()

}

t := template.Must(template.ParseFiles("layout.html", "pageData.html"))

ここで、以下の二個のtemplete処理対象ファイルを読み込みます。

<layout.html>

<pageData.html>

mux.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/"))))

スタイルシートを読み込むために検索パスを追加します。

 

実行すると、例えばこんな具合にブラウザ上に表示されます。

これでNode.jsと同等レベルになったので、Golang上で機能を追加していきます。

 

admin

M5StackC plusとラズパイ Model B+をつなぐ

いずれMyfareカードリーダーのアプリはラズパイで動かすので、つないでみた。まずはUSBがtype-Aで物理的に接続の簡単なラズパイ Model B+でつないでみます。最初I2Sのオーディオ拡張ボード挿入したままだと電源入らずラズパイも起動しないから、拡張ボードを外すと起動できました。

外部電源が不足か、ラズパイ本体から電力供給できないかのどちらかですが、USB type-Aにはmini-Bの5Vから直接供給しているので外部電源の容量不足のようです。

ラズパイからの見え方は、

$ lsusb
Bus 001 Device 005: ID 0403:6001 Future Technology Devices International, Ltd FT232 Serial (UART) IC
Bus 001 Device 003: ID 0424:ec00 Microchip Technology, Inc. (formerly SMSC) SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Microchip Technology, Inc. (formerly SMSC) SMC9514 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

Device 005が該当で、当然ながらMacで読んだ値と同じで、

port.VID == “0403” && port.PID == “6001”

となります。

他の要因もあるから、Macでクロスコンパイルしたバイナリがそのまま動くかどうか?

 

admin

MARS(Multi-Application Recovery Service)@qnap

MARSというソフトがQNAPから昨年秋にリリースされていて、使い道はgoogle photoとwordpressのバックアップに使えそうです。

で、NASのディレクトリ再構築の機会にインストールしました。

https://www.qnap.com/ja-jp/how-to/tutorial/article/marsmulti-application-recovery-serviceを使用してgoogle-フォトのストレージの容量を確保するにはどうすればよいですか

に手順が記載されていますが、スケジュール設定すればほぼリアルタイムバックアップもできます。HBS3との違いはプロトコルが違うのと、バックアップ限定で同期処理はできないというのが機能面の違いでしょうか。

wordpressのバクアップ設定はこれから、

 

admin

GORMで既存のSQLite3のテーブルにアクセスする

他のGolangでSQL文を使って作成したテーブルにGORMを使ってアクセスします。ポイントはテーブル名がデフォルトでは使えないので、Tabler interfaceのメソッドであるTableName()を実装してテーブル名を与えているところになるかと思います。

https://isehara-3lv.sakura.ne.jp/blog/2023/04/01/goのormであるgormを見てみる/

の再構成に過ぎませんが、最低限やりたいことはGORMで実現できそうです。

package main

import (
	"fmt"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"time"
	"strconv"
)

type Product struct {
	Uid		string		`gorm:"primaryKey"`
	Name  	string	
	Time 	int
	Stat	int
}

var products []Product
//
// implements TableName of the Tabler interface 
//
func (Product) TableName() string {
	t := time.Now()
	tableName := "tbl" + strconv.Itoa(t.Year()) + strconv.Itoa(t.YearDay())
	return tableName
  }

func main() {
	db, err := gorm.Open(sqlite.Open("~~~path to the db~~~ /myfare.db"), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}

	// Migrate the schema
	db.AutoMigrate(&Product{})		// if you use Automigrate and change struct, it won't be reflected automatically

	db.Debug().Find(&products) 					// SELECT * FROM tbl*****;
	for i, p := range products{
		//db.Model(&p).Update("Qty", 20)
		fmt.Println(i, p)
	}

}

 

admin

NASのHDDを2.5″から3.5″に入れ替え

timemchine の容量が半年ぐらいで一杯になるので、公称1TBの2.5″から公称6TBの3.5″に入れ替え。

ついでにディレクトリの入れ替えもやろうとしたらシステムを初期化してしまったから再度ソフトインストール。

二台のMacBookでボリュームは共有(5.4Tぐらいを)するようにしてできるだけ効率よくストレージ容量を使うようにしてみた。これでタイムマシーンも3年ぐらいは問題なくバックアップできるはずで、それ以上は本体の買い替えになる可能性も大きいから不要かな。

 

admin

GoのORMであるGORMを見てみる

ORM(Object Relational Mapping)というのは、SQL文ではなくオブジェクト指向でデータベースを操作できる機能です。

Go言語ではGORMがもっともポピュラーなようなので、見てみましょう。

https://gorm.io/ja_JP/docs/index.html

にあるサンプルに多少手を入れて動作を理解しようとしたものです。

package main

import (
	"fmt"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type Product struct {
	gorm.Model
	Code  string
	Price uint
	Qty uint
}

/*
// gorm.Modelの定義
type Model struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
}*/

var products []Product

func main() {
	db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}

	// Migrate the schema
	db.AutoMigrate(&Product{})		// if you use Automigrate and change struct, it won't be reflected automatically

	// Create
	db.Create(&Product{Code: "D42", Price: 100, Qty: 20})
	db.Create(&Product{Code: "A42", Price: 300, Qty: 5})

	// Read
	var product Product
	//db.First(&product, 1) // find product with integer primary key

	db.First(&product, "code = ?", "D42") // find product with code D42
	fmt.Println(product.ID, product.Code, product.Price, product.Qty)

	// Update 
	db.Model(&product).Update("Price", 200)			// update only pointer(*) reocord
	fmt.Println(product.ID, product.Code, product.Price, product.Qty)
	db.Model(&product).Update("Qty", 30)			// *:initial Create record pointer
	fmt.Println(product.ID, product.Code, product.Price, product.Qty)
	db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // non-zero fields
	fmt.Println(product.ID, product.Code, product.Price, product.Qty)
	db.Model(&product).Updates(map[string]interface{}{"Price": 500, "Code": "F42"})
	fmt.Println(product.ID, product.Code, product.Price, product.Qty)

	db.Model(&products).Where("ID = ?", 2).Update("Price", 1000)	// search ID and update record

	// Delete - delete product with condition
	//db.Delete(&products, "Code = ?", "F42")		// delete all of the code = "D42"

	db.Find(&products) 					// SELECT * FROM Product;
	for i, p := range products{
		//db.Model(&p).Update("Qty", 20)
		fmt.Println(i, p)
	}

}

以下が実行結果になります。意外だったのは、db.Modelは現在のID(Primary key)のレコードに対して働くということ。

・実行一回目

1 D42 100 20
1 D42 200 20
1 D42 200 30
1 F42 200 30
1 F42 500 30
0 {{1 2023-04-01 09:25:58.10815 +0900 +0900 2023-04-01 09:25:58.110959 +0900 +0900 {0001-01-01 00:00:00 +0000 UTC false}} F42 500 30}
1 {{2 2023-04-01 09:25:58.10884 +0900 +0900 2023-04-01 09:25:58.111384 +0900 +0900 {0001-01-01 00:00:00 +0000 UTC false}} A42 1000 5}

・実行二回目

3 D42 100 20
3 D42 200 20
3 D42 200 30
3 F42 200 30
3 F42 500 30
0 {{1 2023-04-01 09:25:58.10815 +0900 +0900 2023-04-01 09:25:58.110959 +0900 +0900 {0001-01-01 00:00:00 +0000 UTC false}} F42 500 30}
1 {{2 2023-04-01 09:25:58.10884 +0900 +0900 2023-04-01 09:26:30.669874 +0900 +0900 {0001-01-01 00:00:00 +0000 UTC false}} A42 1000 5}
2 {{3 2023-04-01 09:26:30.666797 +0900 +0900 2023-04-01 09:26:30.669524 +0900 +0900 {0001-01-01 00:00:00 +0000 UTC false}} F42 500 30}
3 {{4 2023-04-01 09:26:30.667772 +0900 +0900 2023-04-01 09:26:30.667772 +0900 +0900 {0001-01-01 00:00:00 +0000 UTC false}} A42 300 5}

データベースは存在しなければgorm.Opendで自動作成されます。

db.Createがレコードの新規作成、db.Modelがレコードの更新、使ってませんがdb.Deleteがレコード削除です。条件付けはwhere句を使うのでSQLと類似しているように思います。またdb.Firstはレコードを一個だけ(SELECT * FROM  products ORDER BY id LIMIT 1;と等価)引っ張ってくるしde.Findはselect*from相当です。

gorm.Modelを構造体に記載するとプライマリーキーや時刻が自動的にレコードに追記されますが、いまいち余計な感じもします。またdb.AutoMigrateはテーブル作成後に構造体を変更しても反映されないようなのでこれも注意が必要でしょう。したがってサンプルのままに使用することはそれほどないように思います。

gorm.Modelを使わなければ、例えば以下のようにP-Keyをセットすれば良いでしょう。

type Product struct {
	ID		int					`gorm:"primaryKey"`
	Code  string	
	Price uint
	Qty uint
}

いずれにしてもSQL文を作成するよりは間違いが入り込みづらいように見えるので、できる限りORMを使うのが良さそうです。

P.S. 2023/4/2

    1. テーブル名の指定

テーブル名ってどうなるかと思ったら、構造体の複数形(ドキュメントの説明:構造体 User の場合、対応するテーブル名は規約により users となります。)が使われるようです。テーブル名をダイナミックに設定したいと思ったらちょっと不便かもしれない。

sqlite> select*from products;
1|F42|500|30|2023-04-02 10:54:48.693791+09:00
2|A42|1000|5|2023-04-02 10:54:48.694968+09:00
3|F42|500|30|2023-04-02 14:13:44.198783+09:00
4|A42|300|5|2023-04-02 14:13:44.200078+09:00

テーブル名はTabler インターフェイスの実装で変更できるようですね。

https://gorm.io/ja_JP/docs/conventions.html

以下を追加すると、

func (Product) TableName() string {
	t := time.Now()
	tableName := "tbl" + strconv.Itoa(t.Year()) + strconv.Itoa(t.YearDay())
	return tableName
  }

テーブル名の変更指定ができました。

sqlite> .tables
products   tbl202392

2. SQL文を出力したい

通常はSQL文は隠蔽されますが、Debug()関数を挟んでやれば、SQL相当のステートメントを出力してくれます。

	db.Debug().Find(&products) 					// SELECT * FROM Product;
	for i, p := range products{
		fmt.Println(i, p)
	}

こんな感じで、



2023/04/02 20:21:46 ~~~/orm.go:71
[0.118ms] [rows:10] SELECT * FROM `tbl202392`

~途中省略
5 { ゲスト5 1680422277 0}
6 { ゲスト6 1680422277 0}
7 { ゲスト7 1680422277 0}
8 { ゲスト8 1680422277 0}
9 { ゲスト9 1680422277 0}

 

admin

GolangとNode.jsでmifareカード使ってチェックイン表示

Mifareカードリーダーを使ったチェックインシステムを
GolangとNode.js使って雛形もどきを作ってみた。Node.jsはwebアプリ雛形作成のexpress-generatorを使っています。

GolangでSQLite3のデータベーステーブル作成とアップデート、Node.jsはwebサーバー起動してviewを作成しています。

・index.js:データベースのテーブルから読み出したチェックイン情報を読み出して、ページ(view)の必要情報を用意します。データベース読み込みは非同期処理なので、async/awaitで同期化します。

・index.pug:index.html作成のテンプレートです。pugファイルにはJavaScriptの記述もできて繰り返し処理が簡単にできるから便利です。

・style.css:スタイルシート、本来のディレクトリから一個上の階層に移動してあります。GitHubへの転送がそうしないとうまくできなかったから。

・serial.go:M5StackC plusからのカード読み取り情報を受け取りjsonのuidと名前対応情報から手ブルを作成。テーブル名は年号と1月1日からの経過日を組み合わせたものにしています。

コードは分散していますが以下のリンクから、

https://github.com/chateight/golang/tree/master/serial

P.S. 2023/4/1

Unmarshalは簡単に使えてこのケースなら別に問題もないですが、巨大なファイルになるとメモリ大量に消費するだろうから効率考えたらストリームとして処理するのが良いようです。

 

*_tool.jsはデバッグ用のコードです。

カードリーダーのM5StackC plusのコードなどの情報は、

https://github.com/chateight/rfid

 

admin

SQLite3の非同期処理を同期化させる(Node.js)

便利なようで結構不便なJavaScriptのシングルタスクを有効に使おうとするイベント処理。非同期処理は簡単な使い方では不便でSQLite3のDBアクセスライブラリには同期型も存在するらしい。

ユーザーが多いだろうから非同期型のライブラリでなんとかしようとするとPromise処理が必要になります。最新の機能ではawait/asyncになりますが。以下はexpress-generatorで発生されたwebページのディスパッチ処理を行うindex.jsにデータベースアクセス処理を同期させて追加したコードです。

<index.js>

var express = require('express');
var router = express.Router();

let nameArray = new Array();

/* GET home page. */
router.get('/', function(req, res, next) {
  var response = Math.floor(Math.random() * (100 - 1)) + 1;

  let t1 = new Date();

async function dbReadExec(){
  try{
    nameArray = await dbRead();
    let t2 =new Date();
    console.log("dbReadEec",nameArray, t2 - t1);
    res.render('index', { title: 'Express', data: response, db: nameArray});
  } catch (err){
    console.log('error');
  } finally {
    console.log("done");
  }
}

dbReadExec();

});

//
// DB handler
//
function dbRead(){
  return new Promise((resolve, reject) => {

  const sqlite3 = require("sqlite3");
  const db = new sqlite3.Database("/Users/username/go/src/serial/myfare.db");
  
  // calc file name
  let d = new Date();
  let year = d.getFullYear();
  let month = d.getMonth();
  let day = d.getDate();
  
  let date1 = new Date(year, 0, 0);
  let date2 = new Date(year, month, day)
  let behind = Math.floor((date2 - date1) / (24*3600*1000));      // convert milisec to day
  
  nameArray = [];       // clear Array
  
  db.serialize(() => {
  db.each("select * from tbl" + year + behind + " where stat=0", (err, row) => {
    if (err) {
      reject(err);
    }
  //console.log(`${row.id} ${row.name} ${row.time} ${row.stat}`);    
  nameArray.push(row.name);
  resolve(nameArray);
  })
  
  db.close();

  });
  });
}


module.exports = router;

serialize()したつもりでも、resolve(nameArray);をdb.close()の後にするとうまく動きません。ここも非同期で動くから?ループ内でresolve()を複数回発行することになりますが、これはthenがchainされて最後のresolve()が有効になるようです。データ量が少ないなら、db.each()ではなくdb.all()で一回で返せば良さそうに思いますが。

let t2 =new Date();

console.log("dbReadEec",nameArray, t2 - t1);

はデータベース処理時間計測のためのタイムスタンプです。二回目以降はJavaScriptのキャッシュが有効だろうから高速です。

シングルタスク言語というのはちょっと複雑な構造のコードを書こうとすると不便なところが目立つように感じます。

 

P.S. 2023/3/28

db.each部分だけをdb.allに置き換え、

  db.all("select * from tbl" + year + behind + " where stat=0", (err, rows) =>{
    rows.forEach(row => nameArray.push(row.name));
    //console.log(nameArray);
    resolve(nameArray);
})

こちらの方がスマートでしょう。

 

admin