Life is Really Short, Have Your Life!!

ござ先輩の主に技術的なメモ

SQLのINとEXISTSの違い

ユーザーテーブルと、そのユーザーが好きなプログラミング言語というテーブルがあるとします。

ユーザーテーブル(UserTable)
id name
1 山田
2 村上
3 中村
好きなプログラミング言語テーブル(FavLangTable)
id user_id lang
1 1 Python
2 1 PHP
3 2 TypeScript
4 2 C#
5 2 Java
6 3 TypeScript

INとEXISTSの使い分け

結合先のレコードを全て欲しい場合は、EXISTS。そうでない場合は、INを使う。
TypeScriptが好きなユーザーを引いてくるSQLをINで書くとこうなる。

SELECT
	ut.name,
         f.lang
FROM
	user_table AS ut
	JOIN fav_lang_table AS f
	ON f.user_id = ut.id
WHERE
	f.lang IN("TypeScript")

結合先をINで絞っているので、そのレコードのみが返却されるため、こうなる。

name lang
村上 TypeScript
中村 TypeScript

TypeScriptが好きなユーザーを抽出するならこれでいい。だが、TypeScriptが好きなユーザーの「他の全てのプログラミング言語」を同時に取得したい場合、うまくいかない。結合先をINで絞り込んでしまうと、結合先のレコードしか取得しないので。

こーゆー時は、EXISTSの出番。EXISTSはそのレコードの存在有無だけを見てくれるので、JOINした結合先のレコードを丸々取ってくる。

SELECT
	ut.username,
         f.lang
FROM
	user_table AS ut
	JOIN fav_lang_table AS f
	ON f.user_id = ut.id
WHERE
	EXISTS (
		SELECT 
			* 
		FROM 
			fav_lang_table AS f1 
        WHERE 
        	f1.lang IN("TypeScript")
        AND
        	f1.user_id = ut.id
)

こうなる。

name lang
村上 TypeScript
村上 C#
村上 Java
中村 TypeScript

EXISTSはINに比べると可読性が下がる(ぱっと見て内容がわかりにくい)が、使いこなせると楽しいよ。

SQLAlchemyのEager Loadingをいい感じにやる

SQLAlchemy==1.4.37

N+1のlazy fetchをしたくない場合、eager loadというJOIN時に予めデータを持ってくる方式があります。SQLAlchemyでも、Railsでも、この辺は一緒。Alchemyの場合、孫のテーブルをJOINした時もそれらが出来る。

数時間苦戦したので、Eager Loadingの設定をメモる。

filterする対象のテーブルが自分だけ

この場合は、joinedloadでいけた。

session.query(UserTable)
    .options(
        joinedload(UserTable.attention),
        joinedload(UserTable.friend_type),
        joinedload(UserTable.bookmark),
    )
    .filter(UserTable.user_id == user_id)

UserTableを軸に、Where句で引いてくるのがUserTableのカラムのみ。

filterする対象のテーブルが複数ある

この場合は、以下のようなjoinedloadを使うと、えらいことになった。

session.query(UserTable)
    .options(
        joinedload(UserTable.attention),
        joinedload(UserTable.friend_type),
        joinedload(UserTable.bookmark),
    )
    .filter(UserTable.user_id != user_id)
    .filter(UserFriendTypeTable.friend_type.in_(["AAA","BBB"])

えらいことになるというのは、filterで指定したテーブルが OUTER JOINではなくCROSS JOINされていたため。FROM user_table, user_friend_type_table というSQLになるので、激重になった。

複雑な検索条件に基づいてフェッチする場合は、joincontains_eagerの合わせ技が良いみたい。

session.query(UserTable)
    .outerjoin(UserBookmarkTable, UserBookmarkTable.user_id == UserTable.id)
    .join(UserFriendTypeTable, UserFriendTypeTable.user_id == UserTable.id)
    .options(
        contains_eager(UserTable.bookmark),
        contains_eager(UserTable.friend_type),
    )
    .filter(UserTable.user_id != user_id)
    .filter(UserFriendTypeTable.friend_type.in_(["AAA","BBB"])

自分はいつもwith_entitiesでカラムを指定してSELECT文を投げることが多かった。一覧画面に表示するためのSQLが多いので、JOIN先がN件でも親に合わせるためにカラムを指定してた。ただ、今回は、要件が特殊でカラムを指定するのではなく、N+1せずに結合先のレコードをリストで保持して置く必要があったので、苦戦したのだった。

これでだいぶRelationshipには慣れてきたな。ドキュメント、じっくり読もう。どこかで。

Asynchronous Gap on BuildContext

Flutter2.x系からこの問題がフィーチャーされるようになった気がする。1.xの時はそこまで。

どんな問題?

  • 非同期でサーバーにデータを送った後、ダイアログを閉じるようなケース。
  • 非同期でawaitを入れた所で、Flutterのウィジェットツリーの再構築が同期されたかは、全くもって保証されない。
  • そのため、非同期処理後に Navigator.of(context).pop() とかやると例外が発生する。
TextButton(
    child: const Text("いいえ(エラーになる)"),
    onPressed: () async {
        await doSomething();
        Navigator.pop(context); //このcontextが同期されない
    }
)
At this point the state of the widget's element tree is no longer stable.
To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method

どーするか

公式の推奨は、StatefulWidgetWidgetのライフサイクルを管理する方式。

dart-lang.github.io

void onButtonTapped() async {
    await Future.delayed(const Duration(seconds: 1));

    if (!mounted) return;
    Navigator.of(context).pop();
  }

StatefulWidgetを使わない場合、awaitする前の Contextをキャッシュすると良いみたいなStackOverflowがあったけど、そもそもキャッシュするもんじゃないだろこれっていう思いが強い。

onPressed: () async {
  final navigator = Navigator.of(context); // store the Navigator
  await showDialog(
    context: context,
    builder: (_) => AlertDialog(
      title: Text('Dialog Title'),
    ),
  );
  navigator.pop(); // use the Navigator, not the BuildContext
},

stackoverflow.com

WPFでコンボボックスの選択値が共有できない

印刷プリンタのコンボボックスを作っていて、その選択値を様々な画面で使い回したいのだが、非常にハマった。今も解決できない。

ViewModelBaseというクラスを作り、そのプロパティを継承したら行けるかと思ったが、ダメだった。ViewModelBaseのコンストラクタのログを見ると、親クラスの値がそのままセットされているけど、画面が表示されるタイミングでSetPropertyを叩かないと無駄っぽい。

まぁそれはいいやで、A画面でプリンタを選び直して、B画面を表示しても、A画面で選択されたプリンタの選択値の通り画面が切り替わらない。アプリケーション全体にまたがってデータをバインドするっていうことが、WPF/Prismでは出来ないのだろうか。解決の方向性が全く見えない。

・・・これ、どうしたらいいんだろうなぁ。

SQLAlchemy2.0スタイルメモ〜scalars〜

FastAPIならasyncで全部行きたいねってことで、プロダクトのコードをFlask→FastAPIに書き直している。

で、簡単なクエリの動作確認で、こんなコードを書く。

async def get_posts(db: AsyncSession) -> List[Post]:
    result: Result = await db.execute(select(Post).order_by(Post.postdate.desc()).limit(20))
    return result.all()

この時、JSONになった文字列を見ると、こういう感じで返ってくる。

{ "Post": 
    {
        "id": 1,
        "title": "aaa",
    }
}

なんだこのPostとかいうエンティティ名は。そんなのいらん。何だこれって思って公式見たら、こうやって書くらしい。

async def get_posts(db: AsyncSession) -> List[Post]:
    result: Result = await db.execute(select(Post).order_by(Post.postdate.desc()).limit(20))
    return result.scalars().all()

こうすると、JSONにした時にエンティティ名のPostが外されて、{"id":1,"title":"aaa"}になった。select関数の中で、配列でカラム名を指定する場合にscalarsを使うと、選んだカラムの1つしか返ってこない。まぁ、スカラー値だもんな。オブジェクトの場合はオブジェクトが返り、Selectした場合は最初の値が返ってくる・・・ってなんだこれ。でも、Alchemy使う以上は従うしか無いし。TypeScriptオンリーに早くなりたい。

EPSON VP-4300で「EJL 1284.4@EJL」という文字列が常に印字される

USBでドットインパクトプリンタとWindowsマシンがつながっている状態で、プリンタの共有をかけた。この共有プリンタをインストールしたクライアント側で、必ず EJL1284〜 とかいう文字列が出るようになってしまった。

ぐぐってみたら先駆者がいた。 okwave.jp

以下のような設定画面になるので、パケット通信設定をオフにしたら、この文字列は出なくなった。この文字列はプリンタ制御コードで、何のエラーかわからんが、通信時に発生し、その内容を人間様に教えるために印字されていたようだ。

EPSON的には仮想USBポートでプリンタ共有かけるのではなく、ローカルポートで ¥¥HogePrintServer¥EPSON-ESCPとかやるのが推奨手順らしい。

以下、マニュアル。 https://www2.epson.jp/support/manual/data/dot/vp1900/BPS0141_00_JA.PDF

OKIのMICROLINEで同じ設定をした時は、このような現象は発生しなかった。や沖1。

FlutterでUnixTimeをDateTimeに変換する

Firebaseのライブラリが国際化対応しているため、日時をUnixtimeで持っていた。 それをDartでDateTimeに変換するときのコードで、癖があったのでメモ。

www.kindacode.com

 final timestamp1 = 1627510285; // timestamp in seconds
 final DateTime date1 = DateTime.fromMillisecondsSinceEpoch(timestamp1 * 1000);

fromMillisecondsSinceEpoch はミリ秒を期待しているが、unixtimeが秒までの時間しか持っていないので、1000倍しないとミリ秒が計算されず復元できないというだけの話。