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

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

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










とある要件を実現するツールを作りました。同じことに悩む人がいた場合を想定して、ツールをソースごと公開することにしました。

某ウェブプラットフォームのサービス終了が決まり(わかる人はこれだけで何の話か推測されそうだけど・・)、現在稼働中のウェブアプリケーションを引っ越しすることになりました。引っ越しそのものはさほど難しくないのですが、問題は「サービスの URL が変わってしまう」ことでした。

図に示すとこのような感じです。これまで運用していたサービスの運用環境を A から B に引っ越しした結果、これまでの URL とは異なる URL で引き続き運用することになりました:
2022070601


これまで使っていたユーザーに対しても「サービスの URL が変わった」ことを知らせてあげたいのですが、具体的にどうするべきでしょうか?A で運用中の画面に「URL が変更になった」と注意書きを含めて、改めて B にアクセスしてもらうこともできます。が、もう少し気の利くやり方として「A にアクセスしたら自動的に B に転送させて、B で運用中の画面で URL が変更になった旨を記載しておく(そのままブックマークできるようにする)」という方法もあります。


この後者の方法を実現するためには A にアクセスした利用者に対して "301" という HTTP ステータスコードと、続けて変更先の URL を Location ヘッダに含めて返すことで実現できます。この HTTP ステータスコード 301 は "Moved Permanentaly" を意味していて「URL が(一時的ではなく)恒久的に変更になった」ことを示しています。続けて Location ヘッダに新しい URL を含めておくことでウェブブラウザ側で新しい URL に自動的に遷移してくれます。つまり「A にアクセスしたら自動的に B に移動させる」ことが実現できます(そして B 側で「URL が変わったので現在のページをブックマークして」といったメッセージを記しておく、といった対応になります)。ウェブページの引っ越しを行う場合の一般的な手段でもあります:
2022070602



問題となるのは、この「301 という HTTP ステータスコードを返す」機能です。A 側のサービスでそのような転送機能や転送の設定が提供されていればそれを使えばいいのですが、必ずしも提供されていないことも考えられます。そのようなケースに対応するため、今回「アプリケーションの機能として 301 HTTP ステータスコードと、引っ越し先 URL を返すアプリケーション」を作ったので、公開することにしました。これまで A で動いてたアプリケーションの代わりにこのアプリケーション(下図の app)をデプロイすることで、A へのアクセスがあった場合に、新しい B への URL に無条件で転送させることができるようになります:
2022070603


このような挙動を実現するための Node.js アプリケーションのソースコードを以下で公開しました:
https://github.com/dotnsf/301movedpermanently

動作確認する場合は、Node.js が導入済みの環境にソースコードをダウンロードするか git clone して、環境変数 URL に転送先の URL を指定して実行します。なおアプリケーションはデフォルトで 8080 番ポートで待ち受けますが、このポート番号を変えたい場合は環境変数 PORT に指定して実行してください:
$ git clone https://github.com/dotnsf/301movedpermanently

$ cd 301movedpermanently

$ npm install

$ URL=https://www.yahoo.co.jp/ node app (全てのリクエストを Yahoo! トップページに転送する場合)

起動後にアプリケーションにアクセスすると、全てのリクエストが環境変数 URL で指定した値(上の例だと https://www.yahoo.co.jp/)に転送されます。なおコード内ではメソッド/パス/パラメータに関係なく、全てのリクエストを GET リクエストに変換して転送しています。GET/HEAD メソッド以外のリクエストについては HTTP ステータス 308 を返して対応するケースもありますが、今回のような「サービスごと引っ越し」のケースでは新 URL のトップページに転送することが多いと思うので、今回は説明を控えます(下の青字部分が実行されます):
app.all( '*', function( req, res ){
  if( post_redirect ){
    var method = req.method;
    if( method == 'GET' || method == 'HEAD' ){
      //. https://developer.mozilla.org/ja/docs/Web/HTTP/Status/301
      res.status( 301 );
    }else{
      //. https://developer.mozilla.org/ja/docs/Web/HTTP/Status/308
      res.status( 308 );
    }
  }else{
    res.status( 301 );
  }

  res.set( 'Location', url );
  res.end();
});

古いサイト A が動いている間だけ有効な強制転送方法ですが、他に方法がない場合はこんな感じで古いサイトを訪ねた人を強制的に B へ転送する方法が有効だと考えています。

なお、docker イメージとしても公開しているので、移行元で docker が使える環境であればこちらを使っていただくのが手っ取り早いと思っています:
dotnsf/301movedpermanently


このページのトップヘ