私が歌川です

@utgwkk が書いている

ふつうの日記を長らく書いていなかったので、書く。

緊急事態宣言が解けたけど、お店はマンボーで19時ラストオーダーのところが多くて、したがって週末ぐらいしか飲みに行けない。もうちょっとの辛抱ではありそう?

ワクチンを打ちたいけど、直近の情勢を見るにいつ打てるかはなかなか確定しなさそう。副反応がひどいとしょんぼりしそうだけど、副反応が出ると「しっかりやってるね~」という感じになりそう。副反応が出たときの休暇制度があるのでバシバシ活用したい。

週末にBREWDOG PUNK IPAを飲んで気持ちを高める暮らしを続けている。この世にまたとない最高の飲み物だと思う。

ここで一句

Relay + TypeScriptでnodeクエリを使うときは__typenameフィールドを明示的に取得すると便利

タイトルが全てです。

作品詳細ページで、id をもとに作品 (Artwork) を取得したいとする。 ArtworkNode インタフェースを実装しているなら、以下のように node クエリを使って取得できる。

query ArtworkDetailQuery($id: ID!) {
  artwork: node(id: $id) {
    ... on Artwork {
      id
      title
      caption
    }
  }
}

が、このクエリをもとにrelay-compilerでTypeScriptのコードを生成すると、以下のように artwork のフィールドが全てoptionalになった型定義が生成されてしまい、不便である。

export type ArtworkDetailQueryResponse = {
    readonly artwork: {
        readonly id?: string;
        readonly title?: string;
        readonly caption?: string;
    } | null;
};

こういう場合は、node クエリで取得する Node オブジェクトの __typename フィールドを取得すればよい。

query ArtworkDetailQuery($id: ID!) {
  artwork: node(id: $id) {
    __typename
    ... on Artwork {
      id
      title
      caption
    }
  }
}

こうするとrelay-compilerが以下のようなunion typeを出力してくれる。使う際は __typename で型を絞り込むことができる。

export type ArtworkDetailQueryResponse = {
    readonly artwork: ({
        readonly __typename: "Artwork";
        readonly id: string;
        readonly title: string;
        readonly caption: string;
    } | {
        /*This will never be '%other', but we need some
        value in case none of the concrete values match.*/
        readonly __typename: "%other";
    }) | null;
};
const { artwork } = usePreloadedQuery(...);

if (!(artwork && artwork.__typename === "Artwork")) {
    return <div>artwork not found</div>;
}
// artworkはArtwork型のオブジェクトであることが確定する

このコンパイラの挙動はrelay-compiler 11.0.2で確かめた。めっちゃフィールドがoptionalになるけどなんとかしたいね、と思って試しに __typename を取得してみたらこの挙動に気づいた。

入力フォームに対してIMEの確定を待ってからEnterでなんかするやつ

入力フォームがあって、Enterを押して入力したテキストをリストに追加したいとしましょう。↓こちらでお試しできます。

  • ここに追加されるよ

こういう感じで実装できます。簡単ですね。

(() => {
  const list = document.querySelector("#list_1");
  const input = document.querySelector("#input_1");

  input.addEventListener("keydown", (e) => {
    if (e.key !== "Enter") {
      return;
    }

    e.preventDefault();

    const text = e.target.value;

    const li = document.createElement("li");
    li.textContent = text;
    list.appendChild(li);

    e.target.value = "";
  });
})();

ところでこの実装には問題があって、IMEの変換をEnterで確定すると、環境によっては確定直前? 直後? の文字がそのまま追加されてしまうことがあります。

以下はMacのGoogle Chrome 91での動作例です。AquaSKKの確定直前の文字 ▼犬 が追加されてしまっています。これはSKKに限らず、たとえばMacデフォルトのIMEでも同じ問題が発生します。困りましたね。

gyazo.com

IMEの変換中はEnterを押してもリストに追加されてほしくないですね。いいプロパティがないか探してみると KeyboardEvent.isComposing というものがあることに気づきます。

developer.mozilla.org

IMEによる変換中は真を、そうでない場合は偽を返すようです。これは使えそうですね。そうして改良した入力フォームが↓こちらです。

  • ここに追加されるよ
(() => {
  const list = document.querySelector("#list_2");
  const input = document.querySelector("#input_2");

  input.addEventListener("keydown", (e) => {
    if (e.key !== "Enter" || e.isComposing) {
      return;
    }

    e.preventDefault();

    const text = e.target.value;

    const li = document.createElement("li");
    li.textContent = text;
    list.appendChild(li);

    e.target.value = "";
  });
})();

これでMacのChromeでも ▼犬 が入力されずに済みました。よかったですね。


ところで、改良した実装でもまだ問題になるケースがあります。Mac Safariで 犬と猫 という文字を追加してみましょう……。

gyazo.com

おや、まだ しか入力していないのにリストに追加されてしまいました。どうやらSafariでは keydown イベントの isComposing プロパティを見る方法でもうまくいかないようです。

2019年の情報ですが、以下の記事が参考になります。Safariでは「Enter押下で確定」時の keydown イベントの isComposing が真にならないようでした。

qiita.com

なんとかできないか、とちょっと考えた末に以下の作戦を考えました。

  • compositionstart イベントと compositionend イベントを自前で監視して、 compositionend イベントが発火した直後かどうかを判定できるようにする
  • Safariでは compositionend イベントの発火直後の keydown イベントを間引く ↑の判定を KeyboardEvent.isComposing の代わりに使う
    • 2021/6/30: 「 keydown イベントを間引く」よりも「自前で compositionend イベントを監視して判定する」の方が表現として適切そうだったので修正しました

これを試したのが↓の入力フォームです。いかがでしたか?

  • ここに追加されるよ
(() => {
  const list = document.querySelector("#list_3");
  const input = document.querySelector("#input_3");

  // SafariっぽいUAのとき、compositionend イベントの直後かどうか判定できるようにする
  const isSafari = navigator.userAgent.includes("Safari/") && navigator.userAgent.includes("Version/");

  let isCompositionFinished = true;

  input.addEventListener("keydown", (e) => {
    if (isSafari && isCompositionFinished) {
      isCompositionFinished = false;
      return;
    }
    if (e.key !== "Enter" || e.isComposing) {
      return;
    }

    e.preventDefault();

    const text = e.target.value;

    const li = document.createElement("li");
    li.textContent = text;
    list.appendChild(li);

    e.target.value = "";
  });

  input.addEventListener("compositionstart", () => {
    isCompositionFinished = false;
  });

  input.addEventListener("compositionend", () => {
    isCompositionFinished = true;
  });
})();

こういうことをしなくても済むようになりたいですね。もっといい方法があれば教えてください。

趣味でGraphQL APIを使ったwebアプリケーションを作ってみた感想

GraphQL APIを備えたwebアプリケーションを趣味で作っていて、完成したので感想をまとめる。

目次

作ったアプリケーション

サークル内で使うイラストアップローダーを作った。機能としてはこういう感じ。有り体に言えばclosedなpixivである。

  • 作品をアップロードできる
  • 作品には1個以上のイラストがある
  • 作品に「いいね」ができる
  • 作品にコメントできる
  • 作品にタグを付けられる
  • タグで作品を検索できる

もともと同様のアプリケーションが動いていたのだけれど、今回それをリプレイスする形で実装した。

使ったライブラリ

Flask

Pythonの薄いwebアプリケーションフレームワークである。RubyでいうところのSinatraポジション。使い慣れているので採用した。

SQLAlchemy

これまではO/Rマッパーは使わず生SQLをゴリゴリ書いていく主義だったのだけれど、SQLを気にして書く段階から脱却したいなと思ったので採用した。

結論としては、Graphene-Pythonと連携してかなり楽に実装できたと思う。declarative_base() を使ってモデル定義をPythonのクラスとして書き下す感じで、最初はぎこちなく書いていたけどだんだん世界観が分かってきた。

知見をいくつかScrapboxにまとめている。典型的な多対多関係とか、一対多関係とは別に関係を保持しておきたいとか、こういうユースケースはあると思う。SQLAlchemyのドキュメントがかなり詳しいので、ユースケースに合った書き方が分からないときは関連していそうな箇所をじっくり読むのがよさそう。

scrapbox.io

alembic

SQLAlchemyのマイグレーションツール。

ActiveRecordだとDBマイグレーション機能があるけどSQLAlchemy本体にはなくて、頑張るか何らかのライブラリを併用することになると思う。alembicはSQLAlchemyの開発チームが開発していて、信頼感がある。

SQLAlchemyのモデル定義を書き換えたあと、alembicでマイグレーションスクリプトを生成する、という方法が典型だと思う。スキーマファーストで差分を生成していくみたいなイメージ?

Graphene-Python

PythonのGraphQLフレームワーク。FlaskやSQLAlchemyを含めた各種フレームワークと連携できる機能がある。RelayのGraphQL Server Specificationに準拠するのも難なくできた。

graphene-file-uploadを使うとMultipart Request Specificationに沿ったファイルアップロードも実装できる。

作ってみてどうだったか

思ったよりも楽に作れたと思う。素朴に実装するとN+1クエリの温床になるけど、dataloaderを適切に差し込むとうまくクエリをまとめてくれる。Multipart Request Specificationに準拠するのは最初けっこうハマって、うまくファイルアップロードできなくて結局Flaskのrequestオブジェクトを直接見ていたけど、やり方が間違っていたことに気づいて修正できた。ここに関しては別途記事にしたい。

2021/7/18 追記: ファイルアップロードについての記事を書いた。

blog.utgw.net

作品詳細ページにはいろいろな情報を出したい、というときに、従来なら必要な情報を頑張ってまとめて集めてくる (N+1クエリに気をつけつつ) ことになると思うけど、GraphQL APIだとクエリを1つ書いて適切なフィールドを要求したらできた。↓みたいなクエリと同じことをバックエンドで頑張っていたのか、と思うと感慨深い。

query ArtworkDetailQuery($id: ID!) {
  node(id: $id) {
    ... on Artwork {
      id
      title
      caption
      createdAt
      account {
        id
        name
      }
      illusts {
        edges {
          node {
            id
            imageUrl
          }
        }
      }
      likes {
        edges {
          node {
            account {
              id
              name
            }
          }
        }
      }
      comments {
        edges {
          node {
            text
            createdAt
            account {
              name
            }
          }
        }
      }
      tags {
        edges {
          node {
            id
            name
          }
        }
      }
    }
  }
}

fragmentで共通部分をくくり出せるのもよくて、作品一覧ページの個別作品コンポーネントをrenderするのに必要な情報をfragmentにしておくと再利用できて便利。どこまで分けるかはけっこう思想があるかもしれない。fragmentにしておくと親コンポーネントがフィールドの詳細を知らなくてもよい、と思って作ってみているけどこれでいいのかは分からない。

POSTリクエストがバンバン飛んでくるので、趣味程度ならいいけど、パフォーマンスなど本番導入する際には気をつけることになりそう。とはいえ、重たいクエリを発行できないようにしたり、dataloaderを実装したりすれば案外なんとかなるのだろうか。あるいはGETリクエストで特定のクエリだけキャッシュできるようにするとか?

おわりに

いくつか (6個ぐらい) のモデルがあって互いに関連し合っている、というwebアプリケーションをGraphQLで実装してみた。従来の開発体験とはまた違った感じで新鮮なので、機会があればGraphQL APIを作ってみるとよさそう。

WEB+DB PRESS Vol.123『Perl Hackers Hub』に寄稿した #wdpress

WEB+DB PRESS Vol.123 (2021/6/24 (明日!) 発売) のPerl Hackers Hubに「他言語のライブラリをPerlに移植する」というテーマで寄稿しました。twitter-textをPerlに移植した際に得られた知見をもとに、他言語で実装されたライブラリをPerlに移植する際のコツについて執筆しました。

Perlに限らず他言語であっても、使いたいライブラリやSDKが使いたい言語で実装されていない、ということはしばしばあると思います。そういった際にどのようにライブラリを移植するのか、気をつけるべきことは何か、について6ページにまとめています。特にPerlに移植する際に気をつけることを重点的に書いていますが、Perl以外への移植を目標とする際にも参考にできることが書けたと思います。

見本誌をいただいたのですが、すごく読みごたえのある記事ばかりでした。ぜひお買い求めください。個人的には、HTTP/3やNext.js・GraphQLなど最新の話題が気になっていたところなので、WEB+DB PRESSでキャッチアップできるのがありがたいです。

gihyo.jp

執筆に誘っていただいた id:papix さん、原稿を重点的にレビューしていただいた @inao さん、監修の皆様、技術評論社の皆様にこの場を借りて感謝を申し上げます。

@inao さんを含めた編集部の方々に原稿をレビューしていただけたのが本当によかったです。普段たどたどしく書いている日本語がどんどん洗練されていって、すごい、意味の通った日本語になっていく……と思いました。

昔からブログにはいろいろな記事を書いていたのですが、こうやってPerl Hackers Hubに寄稿する貴重な機会をいただけたのは嬉しい限りです。Perlに関する情報を調べていて何度も参考にさせていただいた連載なので、そこに自分の名を連ねられるのは、恩返しのような心持ちがあります。

繰り返しになりますが、2021/6/24 (なんと明日!!) 発売のWEB+DB PRESS Vol.123をよろしくお願いします。

f:id:utgwkk:20210608213827p:plain

2021/8/25 追記

全文がgihyo.jp上で公開されました。どうぞご覧ください。

gihyo.jp

SQLでSQLを組み立てる

趣味で作っているアプリケーションについて、データマイグレーションを行いたくなった。ちゃんとやるなら、マイグレーション用のスクリプトを書いて、メンテナンスモードにして、スクリプトを適用する……みたいな手順を踏むと思うけど、趣味プロダクトなので高速に済ませたい。SQLでSQLを組み立てればワンタイムスクリプトを書かなくてもよいのではないか、という考えに思い至ったのでメモする。

以下はSQLiteでの事例だけど、他のDBMSでも似たようなことはできると思う。

組み立て方

文字列連結

SQLiteの文字列連結は || 演算子で行う。

sqlite> SELECT 'a' || 'b';
ab

NULLと文字列を連結するとNULLになり、ターミナル上では空行が出力される。 typeof は値の型を返す関数である。空文字列ではなくNULLが出力されていることを確認した。

sqlite> SELECT 'aaa' || NULL;

sqlite> SELECT typeof('aaa' || NULL);
null

文字列のエスケープ

文字列連結でSQLを組み立てると、当然SQLインジェクションのリスクがある。 user テーブルに name カラムがあって、たとえば ' OR 1=1 -- という文字列が格納されていると考える。以下の例ではすでにSQLインジェクションに成功している。

sqlite> SELECT "UPDATE users SET some_flag = 1 WHERE name = '" || "' OR 1=1 --" || "'";
UPDATE users SET some_flag = 1 WHERE name = '' OR 1=1 --'

SQLiteには quote という関数があり、与えられた文字列をクォートして、さらにエスケープも行ってくれる。

sqlite> SELECT "UPDATE users SET some_flag = 1 WHERE name = " || quote("' OR 1=1 --");
UPDATE users SET some_flag = 1 WHERE name = ''' OR 1=1 --'

ちなみにシングルクォートのエスケープは '' である。

sqlite> SELECT 'a''b';
a'b

ここまでの内容で、単純なUPDATE文ぐらいなら書けるようになったと思う。

WHERE IN を書きたい

ところで、こういうSQLを生成したいときはどうしたらよいのか? IN (...) の中には複数の値が入るが、果たしてSQLで書けるのか。

UPDATE users SET some_flag = 1 WHERE id IN (...)

これは group_concat という関数を使うことで実現できる。

まず、対象となるidをかき集めるSELECT文を用意する。

-- 対象となるidをかき集めるSELECT文
SELECT id FROM users WHERE some_condition;

idを group_concat(id) に置換すると、INクエリに突っ込めそうな文字列ができる。

-- 対象となるidをかき集めるSELECT文
sqlite> SELECT group_concat(id) FROM users WHERE some_condition;
2,4,6,8,10,12,14,16,18,20

あとはうまく文字列連結してやればよい。

sqlite> SELECT 'UPDATE users SET some_flag = 1 WHERE id IN (' || group_concat(id) || ')' FROM users WHERE some_condition;
UPDATE users SET some_flag = 1 WHERE id IN (2,4,6,8,10,12,14,16,18,20)

万が一、対象となるidが存在しない場合は group_concat(id) はNULLを返す。従って文字列連結した結果もNULLとなり、空行が出力される。 UPDATE users SET ... WHERE id IN () のようなクエリを実行してしまいエラー、ということにはならないようだった。そもそもSQLiteの場合はエラーにならないが……*1

WHERE IN の中でUPDATEする対象のidをSELECTするのとの違い

SQLを生成する場合は、具体的にどういう行がUPDATEされるのかをクエリやコメントに出力できる。SELECT文だけで何行UPDATEされるかはデータに対する知識がないとすぐには計算できないけど、クエリに直接idが書いてあったら、なんか多すぎておかしい、全くUPDATEされないじゃん、みたいなことに気づけそう。

UPDATE users SET some_flag = 1 WHERE id IN (...) -- ()の中を見たら、UPDATEされたusers.idが分かる

たとえばこういう感じにしたら、対象となるユーザー名も分かってお得。UPDATE文を生成するなら、UPDATEする前の値もメモしておくと後々調査したくなったときに便利かもしれない。

sqlite> SELECT 'UPDATE users SET some_flag = 1 WHERE id = ' || id || '; -- ' || name FROM users;
UPDATE users SET some_flag = 1 WHERE id = 2; -- john_doe
...

まとめ

SQLでSQLを組み立てることでデータマイグレーションを行えそう。生成したSQLをどこかにメモしておけばあとで見返すことができる。

補足 (組み立てたSQLをどう実行するのか)

SQLiteには与えられた文字列をSQLだと思って実行する eval 的な関数はない (当然標準SQLにもないと思う) ので、出力したSQLをコピペして改めて実行することになる。

標準入力から流し込んでもよいし、先頭に BEGIN; を足してからトランザクションを張りつつペーストして実行してもよい。

参考

SQLiteのドキュメントが好きで、よく読んでいる。

Pythonのキーワード引数に任意のdictを展開して渡す

Pythonでは、引数に **kwargs のように書くことで、任意のキーワード引数を受ける関数を定義することができる。 キーワード引数はdictとして使うことができる。

def print_kwargs_as_dict(**kwargs):
    print(kwargs)

キーワード引数では、識別子としてvalidな名前の引数しか渡せない。また、任意の文字列や文字列リテラルを使うことはできない。以下の式はいずれも文法エラーになる。

print_kwargs_as_dict(foo/bar='baz')
print_kwargs_as_dict('foo/bar/'='baz')

ところで、Pythonには任意のdictを展開して関数に渡す文法が定義されている。

kwargs = dict(foo='bar')
print_kwargs_as_dict(**kwargs)
# => {'foo':'bar'}

ここで、関数呼び出しの文法を注意深く眺めると、キーワード引数に任意のdictを展開して渡せることが分かる。つまり以下の式はvalidである。

print_kwargs_as_dict(**{'foo/bar':'baz'})
# => {'foo/bar':'baz'}