Life is Really Short, Have Your Life!!

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

ReactでUIコンポーネント作る粒度の温度差について

Checkboxのコンポーネントを作るになんでこれだけのコード量が必要になるのだろうか... Hooksもふんだんに使われている。

github.com

import React, { useCallback, useEffect, useRef, useState } from "react";
import styled from "styled-components";

import {
  CheckBoxIcon,
  CheckBoxOutlineBlankIcon,
} from "components/atoms/IconButton";

import Text from "components/atoms/Text";
import Flex from "components/layout/Flex";

export interface CheckBoxProps
  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "defaultValue"> {
  /**
   * 表示ラベル
   */
  label?: string;
}

const CheckBoxElement = styled.input`
  display: none;
`;

const Label = styled.label`
  cursor: pointer;
  margin-left: 6px;
  user-select: none;
`;

/**
 * チェックボックス
 */
const CheckBox = (props: CheckBoxProps) => {
  const { id, label, onChange, checked, ...rest } = props;
  const [isChecked, setIsChecked] = useState(checked);
  const ref = useRef<HTMLInputElement>(null);
  const onClick = useCallback(
    (e: React.MouseEvent) => {
      e.preventDefault();
      // チェックボックスを強制的にクリック
      ref.current?.click();
      setIsChecked((isChecked) => !isChecked);
    },
    [ref, setIsChecked]
  );

  useEffect(() => {
    // パラメータからの変更を受け付ける
    setIsChecked(checked ?? false);
  }, [checked]);

  return (
    <>
      <CheckBoxElement
        {...rest}
        ref={ref}
        type="checkbox"
        checked={isChecked}
        readOnly={!onChange}
        onChange={onChange}
      />
      <Flex alignItems="center">
        {/* チェックボックスのON/OFFの描画 */}
        {checked ?? isChecked ? (
          <CheckBoxIcon size={20} onClick={onClick} />
        ) : (
          <CheckBoxOutlineBlankIcon size={20} onClick={onClick} />
        )}
        {/* チェックボックスのラベル */}
        {label && label.length > 0 && (
          <Label htmlFor={id} onClick={onClick}>
            <Text>{label}</Text>
          </Label>
        )}
      </Flex>
    </>
  );
};

export default CheckBox;

個人的にはこれぐらいでよいのではと感じる。propでUIコンポーネントが受け取るパラメータをtypeで定義して代入する。場合によっては、useEffectやuseStateをonChangeに仕込むなりしたらいい。

weev.media

type Props = {
  //idを追加
  id: string;
  value: boolean;
  text: string;
  onChange: () => void;
};

export const Checkbox = ({ id, value, text, onChange }: Props) => {
  return (
    <div>
      <label htmlFor={id}>
        <div>
          <input
            type="checkbox"
            id={id}
            checked={value}
            onChange={() => {
              onChange();
            }}
          />
        </div>
        <div>
          {text}
        </div>
      </label>
    </div>
  );
};

CloudSQL にローカルから接続するメモ

GCP SDKのインストール(任意)

$ brew install --cask google-cloud-sdk
$ source /opt/homebrew/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/path.zsh.inc
$ source /opt/homebrew/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/completion.zsh.inc
$  source ~/.zshrc
$ gcloud --version
Google Cloud SDK 404.0.0
bq 2.0.78
core 2022.09.23
gcloud-crc32c 1.0.0
gsutil 5.14

Cloud SQLに接続可能なユーザーを作成

$ gcloud iam service-accounts create sample-user --display-name "sample-user" --project <PROJECT_ID>
$ gcloud projects add-iam-policy-binding <PROJECT_ID> --member serviceAccount:sample-user@<PROJECT_ID>.iam.gserviceaccount.com --role roles/cloudsql.client
$ gcloud iam service-accounts keys create key.json --iam-account sample-user@<PROJECT_ID>.iam.gserviceaccount.com

Cloud SQL Proxyを入れて、立ち上げる。SSHトンネルのようなものか。

# M1 はこれ
$ curl -o cloud_sql_proxy https://dl.google.com/cloudsql/cloud_sql_proxy.darwin.arm64
$ chmod +x cloud_sql_proxy
$ ./cloud_sql_proxy -instances=<インスタンス接続名>=tcp:13306 -credential_file=key.json

あとはSQLクライアントソフトでつなぐだけ。ローカルホストで13306番に。お疲れさまでした。

Google 認証のしょーもないエラー

Unhandled Exception: PlatformException(network_error, com.google.android.gms.common.api.ApiException: 7:

このエラーが出て、なんだろうと思ったら、なんてことはない、検証機AndroidWifiが切れていて、インターネットにつながっていなかった...

ちなみに、ApiException:10の場合は、AndroidのフィンガープリントがUPされていないか、UPされたフィンガープリントの署名が違うか。

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では出来ないのだろうか。解決の方向性が全く見えない。

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