まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

2022/08

M5Stack シリーズの中でも現時点(2022/08/17)で最も小さな体積を持つ M5Stamp に Obniz OS をインストールしました。その手順を備忘録として残しておきます。
IMG_20220817_132731_130


【環境】
私の PC 環境は Windows 10 で、Obniz Writer v0.9.1 を使って書き込みをしています。デバイスドライバを含めた Obniz Writer のインストール方法についてはこちらを参照ください:
https://obniz.com/ja/doc/reference/obnizos-for-esp32/quick-start/obnizwriter

また今回書き込む対象には M5Stamp シリーズ中の Pico DIY Kit を使いました。この DIY Kit に含まれる ESP32 Downloader がないとイメージの書き込みができないため、複数の M5Stamp Pico に書き込む場合であっても、最低1セットはこの DIY Kit が必要になると思います。なお初めから無制限ライセンスが付属する Obniz Board などとは別に、1ユーザーにつき1デバイスまで無料で(今回のように ESP32 デバイスで) Obniz OS を利用することができます。

Obniz 開発者コンソールのアカウントを所有し、上述の Obniz Writer の導入が(デバイスドライバインストール含めて)完了している前提で以下を紹介します。


【インストール作業】
では実際に M5Stamp Pico に Obniz OS をインストールしていきます。まずは Obniz 開発者コンソールにログインし、左メニューから「管理」-「デバイス」を選択して、現在までに自分のアカウントに紐づいている Obniz デバイスの一覧を表示します。この時点ではまだ M5Stamp Pico は含まれていないはずです:
2022081701


M5Stamp Pico に DIY Kit 付属の ESP32 ダウンローダーモジュールを接続してから ESP32 ダウンローダーと PC を USB ケーブルで接続します。なおこのキットには USB ケーブルは付属していないので、PC と接続するための USB ケーブル(ESP32 ダウンローダー側は USB Type-C)が必要です。接続できると M5Stamp Pico の LED が光ります:
IMG_20220817_135151_187


Obniz Writer を起動して書き込みの準備を行います。以下の内容を指定します:
- COMポート: デバイスドライバ導入後に M5Stamp Pico を接続して認識したポート(おそらく1つだけ)を選択
- Baudrate: 115200 bps
- "Write Obniz OS" にチェック
- Choose Hardware: esp32p
- Choose OS Version: esp32p - 3.5.0(特に理由がなければ最新版)
- "Create New Device" にチェック
- "Link to QR Code" のチェックを外す
- Obniz OS 起動時に無線 LAN に接続する場合は "Write Config" にチェック
  - "Set indivisually" にチェックして、接続情報の書かれた JSON ファイルを指定。ネットワークの設定情報など詳しくはこの辺を参照

m5stamppico_obnizos_1


最後に "Write" ボタンをクリックし、書き終わるまでしばらく待ちます。OS (と無線 LAN 設定を指定した場合はその情報)の書き込みが終了すると、新たに割り当てられた Obniz ID を含めた完了メッセージが表示されます:
2022081702


※管理者コンソール内で OS ライセンスの支払い方法を登録せずに2台目以上の OS を書き込もうとすると以下のような "You need to add payment method" というエラーメッセージになりました。自分の場合は有効だった1台を無効にしてから書き込み直すことで成功しました:
m5stamppico_obnizos_3


正しく書き込みが完了した状態で Obniz 開発者コンソールをリロードすると、先程まで無かった、新しいデバイスが1つ追加されているはずです。これが新たに Obniz OS がインストールされ、Obniz デバイスとして認識された M5Stamp Pico デバイスです(M5Stamp が起動して無線 LAN に接続された状態であれば "Active" なデバイスとして認識されているはずです):
2022081703


もともと Obniz デバイスとして使うために M5Stamp を購入したので当初の目的が達成できました。この DIY キットに含まれていた ESP32 ダウンローダーがあれば(2台目からは OS ライセンスが必要ですが)M5Stamp Pico さえ購入すれば Obniz 化できそうです。自分の場合は Groove アダプタ(これは DIY Kit でなくても付属している)をつけて使うことが多いので、もう少し改良することになると思います。



Node.js + PostgreSQL 環境で複数レコードのバルクインサートを、それも対象テーブルに UNIQUE 制約が存在している場合のバルクインサート方法について調べる機会があったので、ブログにまとめておきます。


【Node.js で PostgreSQL を使う】
まず、Node.js で PostgreSQL クライアント機能を実装する場合、よく使われているのが pg(node-postgres) という npm のライブラリだと思っています:
2022080601


こんな感じでコネクションプーリングを作って、、
  :
  :
var PG = require( 'pg' );
var pg = new PG.pool( 'postgres://localhost:5432/mydb' );
  :
  :


こんな感じで SQL (以下の例は "INSERT" 処理)を実行し、結果を取得します:
  :
  :
if( pg ){
  var conn = await pg.connect();                                      //. コネクション割当
  if( conn ){
    try{
      var sql = "insert into table1( name, num ) values ( $1, $2 )";  //. SQL
      var query = { text: sql, values: [ "木村", 100 ] );             //. SQL パラメータ
      conn.query( query, function( err, result ){
        if( err ){
          console.log( err );                                         //. SQL 実行時エラー
        }else{
          console.log( result );                                      //. SQL 実行結果
        }
      });
    }catch( e ){
      console.log( e );                                               //. 例外発生
    }finally{
      if( conn ){
        conn.release();                                               //. コネクションのリリース
      }
    }
  }
}
  :
  :


今回もこの pg を使って紹介します。


【Node.js + PostgreSQL でバルクインサートを実装する】
上の例では一回の SQL 実行で1つのレコードをインサートする、という処理の例を紹介しました。複数のレコードをインサートする場合もこれを複数回続けて実行してもよいのですが、レコード数が多い場合に効率よくインサートできると便利です。このように「複数のレコードを1回の処理でまとめて作成する」ことを「バルクインサート」と呼びます。

Node.js + PostgreSQL でバルクインサートを実装する方法もいくつかありますが、今回は node-pg-format という npm ライブラリを使った例を紹介します:
2022080602


実際に使う場合はバルクインサートデータを配列で用意して、以下のように実装します:
  :
  :
var format = require( 'pg-format' );

var records = [         //. インサートするレコードを配列で用意
  [ "木村", 100 ],
  [ "鈴木", 101 ],
  [ "木村", 102 ]
];

if( pg ){
  var conn = await pg.connect();                                      //. コネクション割当
  if( conn ){
    try{
      var sql = format( "insert into table1( name, num ) values %L", records );  //. SQL
      var query = { text: sql, values: [] );                          //. SQL パラメータ
      conn.query( query, function( err, result ){
        if( err ){
          console.log( err );                                         //. SQL 実行時エラー
        }else{
          console.log( result );                                      //. SQL 実行結果
        }
      });
    }catch( e ){
      console.log( e );                                               //. 例外発生
    }finally{
      if( conn ){
        conn.release();                                               //. コネクションのリリース
      }
    }
  }
}
  :
  :

上例のように、SQL 文全体を pg-format でインスタンス化しています。その際にインサートする複数レコードを配列データとして付与しています(conn.query() 実行時のパラメータは空です)。これで records 配列変数で指定された複数レコードを1回の SQL 実行でまとめてインサートすることができるようになります。


【Node.js + PostgreSQL のバルクインサートと UNIQUE 制約】
ここからがこのブログエントリの本題に入っていきます。バルクインサートの方法は上で紹介しました。問題はこれではうまくいかないケースがあるのでした。具体的には対象テーブルに UNIQUE 制約が付与されていて、この制約にかかるレコードがバルクインサートの一部に含まれる場合です。

具体的に紹介します。例えば今回は table1 という名称のテーブルに複数レコードをバルクインサートする例を紹介していますが、この table1 テーブル作成時に以下のような指定がされていた可能性は排除できません:
create table table1( name varchar(50) default '', num int default 0 );
alter table table1 add unique( name, num );

1行目の "create table ..." はいいとして、問題は2行目です。この制約を与えたことによって name と num の組み合わせは一意(UNIQUE) でないといけない、という条件が付与されました。name は被ってもいいし、num も被ってかまわないのですが、name と num の組み合わせはユニークでないといけない(この条件に反するデータはインサートできない)ようになりました。

このような UNIQUE 制約が存在している前提で、改めて上例を見てみます。この例でバルクインサートしようとしているレコードは、
var records = [         //. インサートするレコードを配列で用意
  [ "木村", 100 ],
  [ "鈴木", 101 ],
  [ "木村", 102 ]
];

という内容でした(配列の1つ目が name 、2つ目が num としてバルクインサートされます)。ここだけを見るとこの例に限っては(name と num の組み合わせは全て異なるので)問題なくバルクインサートできそうです。 ・・・が、もしこのバルクインサート実行前に既に { name: "木村", num: 100 }
のデータが記録済みであった場合
はどうでしょう?バルクインサートの最初のレコードは UNIQUE 制約にかかってしまいます。つまり実行エラーとなります。

そして更にこの問題をややこしくしている事情があります。それは「バルクインサートが UNIQUE 制約エラーで失敗するのはいいが、その時 UNIQUE 制約にかからなかったレコードはインサートされるべきか?否か? という問題です。この答はおそらくデータやアプリケーション次第、つまりケース・バイ・ケースです。まとめてインサートされないと意味がないレコードであった場合はまとめてエラー(UNIQUE 制約にかからなかったレコードもインサートされない)という挙動が正しい挙動となり、UNIQUE 制約にかかったレコードはインサートされないが、他のレコードはインサートされるべきデータであった場合はバルクインサートの一部だけが失敗し、残りのレコードは挿入されているべき、ということになります。 

ちなみに上例で紹介したバルクインサート実装の場合は前者の挙動となります。つまり「全てのレコードが制約違反にならなかった場合のみバルクインサートが成功し、1つでも制約違反のレコードがあった場合はそうでないレコードも含めて1つもインサートされずにエラーとなる」という挙動です(そもそも厳密には「バルクインサート」とはそのような挙動および処理を意味する言葉です)。


【Node.js + PostgreSQL のバルクインサートと UNIQUE 制約】
長い前置きでした。ではここまでの背景を理解した上で UNIQUE 制約に違反しているレコードのみ挿入されず、違反していないレコードは正しく挿入されるようなバルクインサート(「バルクインサート」の定義としてはおかしいですけど・・)を実装するにはどのようにすればよいでしょうか? これが本ブログエントリの肝となる部分です。

これは実際には使っている RDB やライブラリ、プログラミング言語によって異なるようですが、PostgreSQL 9.5 以上で、Node.js + node-postgres を使っている場合であれば以下の方法で実現できるようです:
  :
  :

var records = [         //. インサートするレコードを配列で用意
  [ "木村", 100 ],
  [ "鈴木", 101 ],
  [ "木村", 102 ]
];

if( pg ){
  var conn = await pg.connect();                                      //. コネクション割当
  if( conn ){
    try{
      //. UNIQUE 制約に違反したデータを無視するような SQL を作る
      var sql = "insert into table1( name, num ) ";
      var selects = [];
      for( var i = 0; i < records.length; i ++ ){
        selects.push( "select '" + records[i][0] + "', " + records[i][1] );
      }
      sql += selects.join( " union all" ) + " on conflict ( name, num ) do nothing";

      var query = { text: sql, values: [] );                          //. SQL パラメータ
      conn.query( query, function( err, result ){
        if( err ){
          console.log( err );                                         //. SQL 実行時エラー
        }else{
          console.log( result );                                      //. SQL 実行結果
        }
      });
    }catch( e ){
      console.log( e );                                               //. 例外発生
    }finally{
      if( conn ){
        conn.release();                                               //. コネクションのリリース
      }
    }
  }
}
  :
  :


この例ではプログラミングによって、以下のような SQL 文を生成して実行しています:
insert into table1( name, num )
  select '木村', 100 union all
  select '鈴木', 101 union all
  select '木村', 102
  on conflict ( name, num ) do nothing

厳密にはバルクインサートではないのですが、1回の SQL 実行で複数のレコードをインサートし、"conflict"(UNIQUE 制約違反)が発生した場合は "do nothing"(無視)するよう指定しています。その結果、UNIQUE 制約違反のないレコードのみがまとめてインサートされる、という処理が実現できています。

バルクインサートで1度に挿入するレコード数が莫大になるようなケース(SQL 文自体が膨大で複雑になるケース)には向かないかもしれませんが、自分の手元では一応期待どおりに動くことを確認しました。ちょうどこういう処理を行いたいアプリを作っていて調べたのですが、他の人の参考になれば。


【参照】
https://stackoverflow.com/questions/53722405/how-to-insert-bulk-rows-and-ignore-duplicates-in-postgresql-9-3










このページのトップヘ