Life is Really Short, Have Your Life!!

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

ユーザー独自属性をどう作るかみたいな話

例えばお問い合わせフォームとかで、名前とメアドは必須とする。ただ、あるユーザーはドロップダウンで予算種別や流入経路を出したい、とあるユーザーはFAX番号も出したい、みたいなやつ。

スキーマレスのFirebaseの場合ユーザーごとにスキーマ作れば終わる(アプリ側は大変だけど)んだけど、RDBはそうもいかないよね。

今まで見たなかで大きく3パターンあるようなので、整理をする。

1. 属性ごとにテーブルが作成する

DrupalというCMSで見たパターン。 属性を追加することでconfigが更新され、configには追加したフィールドの格納場所がわかる。で、Field APIでカスタム属性をを全部引っ張ることができるようだ。

2. metaテーブルを作る

WordPressが得意としている実装。option_metaとかuser_metaとか。

field_namefield_valueを保存するテーブルを作って、キー/値ペアを作るやつです。カラムのスキーマはバイナリ以外は自動的に文字列になるので、そのあたりが微妙。重量みたいなカスタム属性があって、4.3kgである場合、本来はDecimal(10,1)とかなんだろうけど。型情報も保存して、toXXXいれるしか無いんかな?

この種の設計は、SQLアンチパターンとはされているけど、格納する→表示するだけなら、まぁそこまで。検索したいとなると、無理。格納表示ならJSONでよいだろって思い始めている。

SQLアンチパターン EAVと4つの解決策

EAVだと検索性が死ぬので、それらの対応策としては、親のテーブルにtype属性を作って、そのtype属性に応じたテーブルを作りJOINするパターンが有る。STIってやつですかね。単一テーブル継承。

マルチテナントのアプリケーションになっちゃうと、ユーザー単位でSTIが作られるので、テーブルの継承が出ずに、結局EAVにならざるをえない(型が決まらないので)。field_type field_name field_value validation みたいなテーブル作って、そこで頑張る感じかな。。。

3. JSONカラムを作って逃げる

WordPressのカスタムプラグインなんかで、どっかで見た実装。フィールド、ラベル、タイプ、バリデーションなどをJSONで用意して、動的にフォームを組み立てて、値も入れちゃうパターン。実装も単純。

まとめ

  • カラムで吸収:STIJSON
  • レコードで吸収: metaテーブル(EAV)
  • テーブルで吸収: Drupal

EAVになったらJSONで大差ないよ派なので、JSONでいいかなー。

組織が抱える過剰な忖度文化というパワーワードに感銘を受けた件

この会社さんが仰ってること、非常によく分かる。

cgo-gal.com

過剰な忖度文化というフレーズがいいし、上司にそうですねと言わないといけないとか、全く同感。もちろん相槌を打つのはあると思うけど、それしか言わなかったお前は何も表現してねぇだろって話だよね。ほんまそうよ。

別の記事で、Lilyさんという方が至言を仰っていて。

Lilyが思う令和のギャルは、自分が経験に基づいた考え方を、表現にまで落とし込んで、恐れずにリリースしている子かなぁ。

いろんな人がいろんな考えを持っているのは大前提で、「うちはこう思うし、こう表現するから」って、他者の目線を入れずに表現できる子がいいなって思う。 r25.jp

すごいなLilyさん。20年ぐらいマネージャーやってるんじゃないかという視点だよこれ。僕が思うに、ほとんどのプロジェクトで、全く出来てない。それも、自分の考えを表現する訓練を積んでない or 足りていないからだ。思ったことを伝える技術を磨いていない。そういう人が役職上マネージャになってしまうと、そのチームは機能しない。少なくとも錆びついたギアでしか動けなくなる。

「自分が経験に基づいた考え方を、表現にまで落とし込んで、恐れずにリリース」する経験ってのは、ビジネス上だと、「それ、おかしくね?」をキチンと言えるかどうか。

「なんか、おかしくね?」ってすごい大事なサイン。でも、それをストレートに出せる人って、そうそういないんだよね。ナチュラルに出せるという意味。ナチュラルにできる人は、自分は他人と違って良いんだって確信できてる。ギャルマインドってココだよね。どう転んでも自分の表現でいいじゃんってさ。僕なんてそれをブログでやり続けたら色んな人がいろんなことを教えてくれたぜ。

おかしいと思ったことはおかしいって言えない職場で働くの、無理じゃないですか? なんで無理してるのかわかんないよね。おかしいって通したら他の人の立場がとか、それはわかるよ。どうせ辞めるし、それもあるね。でも、おかしいものはおかしいし、筋が通らないのは筋が通らない。誰かにしわ寄せが来るだけ。しわ寄せが来るのはあなたのせいじゃないし、巻き取る意味もないかもしれないけどさ、どうせ次でも「それ、おかしくね?」と向き合う時は来るから。リリースしようぜ、自分の経験に基づいたヘルプをさ。

筋が通らないことをやっていると、時間が経てば経つほど、正しい方向に戻すのは難しくなる。筋が通らないままに作った仕組みなりが出来上がっちゃうから。

もちろん言いっぱなしだと他人批判に取られちゃうから、最低限と根拠と理由、まずは理由が必要かな。おかしさを生み出してる理由がどっかにあるわけで。それを出すから相手に伝わる。いいんよ、理由には根拠がなくたって。理由を裏返して根拠を見つけたらそれで。

よし、これをメインのブログに乗っけていくぜ。いい感じのドラフトだ。

TypeScriptで好みのORMが見つからない問題

TypeScriptでバックエンドを書いちゃう皆様、ORMは何をお使いですか?

私はシェアが大きい Prismaを使ってみたんですが、どうも肌に合いません。その理由は以下のようなものです。

1. 独自スキーマ定義はいらない

ORMの特徴として、テーブルのスキーマに合わせてカラムの型、ENUM、外部キー、ユニーク制約などを生成してくれます。 Prismaはこれを独自のDSLで実現し、npx prisma generateスキーマを生成します。

自分だけかもしれませんが、ORMのスキーマに細かいリレーションシップの定義は不要です。 ActiveRecordhasMany的な定義を事前に入れるのはだるいし、JOIN条件はクエリの内部に書きたい。

自分が求めるのはコンパイルできるSQL(このカラムねぇよ、そんなJOINできませんよ)であって、それ以上の機能は不要。

Prismaのようなスキーマ生成をする場合、その作業をnode.jsのコンテナに入れないといけない。その場合、GAEの無料インスタンスに乗らないんですね。それも個人的な都合でネックでした。だって、無料で充分まわるんだもん、業務系のシステムってユーザー数が少ないからw

2. Prismaは集約系の機能が弱い

業務系システムやWebサービスの管理画面系になると、GroupByを使わない日は無いと思います。Prismaの独自DSLによって、GroupByの条件を指定するのがとてもだるいです。また、GroupByした時、JOINできないのも勘弁してほしかった。CTEとかもどこまでサポートしているか怪しい。

3. DB独自の関数が利用できない

これはPrismaが意図的にやっていることであって、あう・あわないはあると思います。よく使うのが、MySQLだと date_format関数などで、それが使えないのは困った。また、includeした場合のクエリの挙動も独特で、それも合わなかった。ORMだから、ある程度発行できるSQLに制御できない部分があるとは思うけど、ちょっとなっていう。

SQLを意識せずDBから値を取ってこれるメリットを何も感じないんですよね。僕はSQLバチバチに意識したい。クエリビルダ型じゃないといやってことに気づきましたw

Prismaも良いところは色々あって、クライアントを拡張できること。ページネーション用の関数を生やしたり、RLSに対応したり、クエリを発行する前に where tenant_id = 2みたいなマルチテナントやる時に必須のWHEREを付け加えたり、クエリ発行後にログを出したりイベント発行したりできる。ミドルウェア的な機能が欲しい場合、現状Prisma一択だと思う。

今の自分は、それが必要じゃなかった。

というわけで Drizzleに手を出したんですけど、何回やっても、MySQLのautoIncrementを設定したテーブルにINSERTできなくて... Issue上げたけどまったく無視されており、どうしていいかわからん。Issueの数がすごく多くて、怖くて使えたもんじゃない。

github.com

そうなると、残るクエリビルダ型が、KYSELYになる。これが最も自分の好みに近い。良い意味で安定していそう。

kysely.dev

TypeScriptのORMを探すたびは続く。

IOバウンドがメインなら言語の差は誤差じゃね

シングルスレッドが問題になるとしたら、FastAPIが動いているPythonもGILなのでシングルスレッドじゃねって思ったのがきっかけ。asyncioがNodeにおけるノンブロッキングI/Oと等価だと思う。PythonのGIL、3.13で廃止されるんだってね。そうなると、gunicornのようにワーカープロセスを何本かあげて管理するモデルも、お役御免になるかも。

php-fpmやgunicornのように親のプロセスでサーバ起動して、ワーカープロセスに各々リクエストを処理させることで、シングルスレッドでマルチプロセスで並行処理ってのもわかる。Nodeもやろうと思えば出来るしそういうNPMパッケージがあるけど、expressの上にかぶせたっていう話は聞いたことがない。僕は。さーせん。今の時代はリクエストの並行稼働をコンテナ単位でスケーリングしようとするから、Expressどうこうじゃないのかもしれない。

APサーバでやることって、バックエンドのDBとか、別コンテナとかにビジネスロジックの実行を依頼することが多い。9割がIOバウンド。PythonSQL発行すると速いのにTypeScriptでSQL発行すると遅いって、言語が原因ではないでしょ。

SQLを実行しているのDB。SQL自体は早くてレスポンスが遅いなら、コネクションの問題であって言語の問題じゃない。TCP開けるのにPythonが速くてNodeが遅いって意味がわからん。

BFFは必要悪だと考えていて、全部TSで行う致命的なデメリットはない派。IOバウンド主体の処理を動かすなら、パフォーマンス面のデメリットはあまり感じられない。PDF1000ページのPDF作るなら、マルチプロセスでやらないときつかろう... 帳票生成系はNode弱いです。HeadlessChrome起動してHTML->PDFみたいなことやったけど、遅かった。

とんでもないリクエストをPHPで捌いているグラブルのエンジニアチームが「Webサービスにおけるパフォーマンスにおいて、言語の差は誤差です」って仰ってくれたらそれを額縁に入れて飾るのに・・・!

上記仮説が正しいか検証するには大規模プロダクトのバックエンドを全部書き換えないといけないけど、そんな機会がどこにあるんだよなので、自分が受託して請けた範囲でFull TSでやっていきいき。

RemixでVertical Sliceをやろうぜ

UI⇔Application⇔Domain⇔Storageというレイヤーをガッツリ統合し、機能単位での依存を最低限にすることをVertical Sliceと言うらしい。縦の結合は最大化、横の結合は最小化。知らなかった。理にかなっている。

www.jimmybogard.com

一番型の担保が難しいのがフロントで、結局ブラウザがJavascriptしか動かせない。フロントエンドをReact/Vue等でビルドしたら、Vertical Sliceを実現する選択肢はTypeScriptしかなさそう。RailsはHotwireでサーバーサイド側にフロントを寄せることでFEの責務を最小化し、Vertical Sliceの実現例を見せてくれた。力技だけど。

我はRubyがあまり好きではない。第一言語Javaだった我には、Rubyのコーディングスタイルが合わない。コンパイルによる型安全が必要だ。ブラウザにはJavascriptの実行エンジンしか積まれないので、結局JavaScriptでDOMDOMするしかない。というわけで、フロントを作るならTypeScript一択。

問題はバックエンドだ。フロントにReactを採用した場合、いい感じにサーバーサイドのロジックが書けるFWがなかった。React自身はUIを構築するライブラリなので、通信やバックエンドのことは原則専門外。そこで非同期通信とレンダリングの仕組みを提供しますよ旦那ってことで、Next.jsが台頭したという認識。他になかった気がする。4年ぐらい前だと。React QueryとかSWRとかも、その類のものという認識。

バックエンドのビジネスロジックを書くものではないとなると、FlutterでRetfrofitでRestAPIクライアント作って呼び出すのと、開発体験としては同じ感覚。アプリならわかるけど、バックエンドのコードが書けないなら面倒なだけでは・・・と。

Page Router時代のgetServerSidePropsを使うのではなく、T3 Stackで提唱されていたtRPCでエンドポイントを自動生成してバックエンドを呼び出すのを見て、お!これええやん!になってトライしたけど、簡単なコードしか書いているものしかなくて、頓挫しちゃった。UIの更新が思いの外面倒だったのと、牛刀をもって鶏を割くことへの違和感が拭えなかった。

MPAの頃だったらFormの入力値を任意のオブジェクトに型変換し、ORMに突っ込んでレコードを作るのは秒殺。Next.jsになった瞬間に、Hooksがどうとか面倒になる。シンプルなAPIを組み合わせて複雑なことをやるとしたら品質がバラつく。なんか違うんだよなぁというツギハギ感が拭えずにいてNext.js以外の選択肢を探した時に、出会えたのだ。マイハニー、Remix(React Router)に。

Remixの Loader / Actionの設計は、自分が望んでいたものだった。Vertical Sliceを実現する最も優れた手段に思え、書き方が単純なので苦労しない。Hooksをゴリゴリ書くこともない。パラレルにリクエストを走らせることをなるべく許容しないRemixのモデルは、受け入れやすかった。

Remixだからではないけど、モノレポ構成にも出来る。アプリ向けのAPIサーバ、一般向けのWebサイト、管理者向けのWebサイトが単一のレポジトリで、エンティティ、DBスキーマ、バリデーションロジックを共有することが出来る。強い、強すぎる。どうしてもこれをやりたい。Gospel Stack Remixでググろう。サンプルレポジトリが置いてあるぞ。

Next.jsの優位性はあんまりよくわからない。ルーティングが簡単なことぐらいしか思いつかない。Reactに詳しくなればなるほど優位性が見えるのだろうか。RSCでコンポーネント単位でBFFやれますって言われても、loaderでJSON返してCacheしたら良さそうに思えてしまう。ピーキーすぎるというか、RSCじゃないと生きていけないケースってどれだけあるんやろ。ストリーミング?

むしろ、SSGやるならNext.jsって印象。SSRならRemixなのではと。実際Remixで書いてみると、Vertical Sliceが素直に実現できる。これ以外でWebアプリケーション書きたくないンゴ。

Remixはこれから間違いなく伸びていくので、2025年頃が楽しみ

気が変わった。T3 Stackそのものは推して行くが、Theoさんの推す構成ではない。

aroundthedistance.hatenadiary.jp

フルスタックはReact前提に

RailsフルスタックWebフレームワークの扉が開いた。Rails,Cake.Django等は、MVCのMCがメイン。Vが弱い。Vにテンプレートエンジンを使い、jQueryを使ってインタラクティブなUIを作っていたけど、もうそれが最適な時代ではない。ReactでUIをプログラミングする前提に立つと、ルーティングとrevalidateがルート単位で可能なFWが一番開発生産性が高くなる。可能であれば、ビジネスロジックもそこに入れて、だ。

Theoさん提唱のT3 Stackでは、Next.js + tRPCがコアのコンセプト。UIはNext.jsで書いて、スキーマを決めてtRPCでエンドポイントを生やし、サーバー通信を行ってUIを更新する。

Next.jsが得意な人であればこれでよいと思うが、私はNext.jsの状態管理ですごく手こずった。Next.jsは初学者にはかなり情報量が多いFWであり、Next.jsの決めた開発手法に強く従うことを求められるピーキーさが苦手。前提となる技術が抽象化されていて、なんかよくわからん状態に。

でも、ViewをReactで書いてTypeScriptでPOSTするとサーバーサイドのコードが呼び出されてUIの更新ができるなら、それがベスト。そして、Remixを見つけることが出来た。これが自分の欲しい物だった。

Remix推しポイント

クライアントとサーバーサイドが1つのファイルに書ける

やばくね??? コレが一番驚いた。tRPCのようなAPI定義も不要。フォームをPOSTしたらaction関数が実行される。

import { json } from "@remix-run/node";
import type { ActionFunctionArgs } from "@remix-run/node";
import { Form } from "@remix-run/react";
import { addTodo } from "~/todoStore";

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const { todo } = Object.fromEntries(formData.entries());
  await addTodo(todo);
  return json({ ok: true });
}

export default function Index() {
  return (
    <Form method="post">
      <label htmlFor="todo">Item name</label>
      <input type="text" name="todo"/>
      <input type="text" name="description"/>
      <button>Add</button>
    </Form>
  );
}

このコードを書けるようになるまでがすごい大変なんだろうけど、開発体験最高に良かった。

Routingの設計が美しい

RemixのコアはVとC。特にC。React Routerによるクライアント側のルーティングに最高のスパイスが乗っている。Nested Routeなどがそれなんだけど、元々の設計として、ルートを起点に遷移するので、所謂サーバーサイドのミドルウェアを作るのが単純化できる。

Hooksをほとんど使わない

Formの実装がほとんど素のHTMLを変わらないのと、useEffectで副作用って所が、基本的に loader関数で代替できるため。 useRefでFormのDOMを触るなどは普通にあるし、チェックボックスに全部つけるみたいなのも、普通に標準のJavaScript書けばいいだけ。そこまでHooksでやるのめんどいだけだろっていう。

ただ、単純なFormでは表現できないようなものも当然ある。予約カレンダーをvisible:hiddenで持ってポップアップして表示する系のやつとか、トグルで開いた時に初めてデータ取ってくる系のやつとか。そういうのは・・・Hooks!

Hooksで処理すべきところと、Remixに委ねていいところが、実に明確で気持ちよい。これなら戦えるって思った。

CSRが出来ないので、Remixを使う時はBFF(実質Node.jsのアプリケーションサーバ)が必要になる。BFFおけないですっていうCSR前提なら、そもそもNext.jsでもなんでもなくて、Reactで書けば良いんじゃないのとも。

残念なことに「フルスタックフレームワーク」だから、バックエンドまできちんと書いたことがないと旨味が少ない。業務系システムはすごいRemixが向いていると思う。100画面ぐらいすぐ作れそう。やっと、自分が目指す形でWebアプリケーションが作れそう!!!嬉しい。

2024.01.15 追記

CSRが出来ないので、Remixを使う時はBFF(実質Node.jsのアプリケーションサーバ)が必要になる。

嘘でーーーす!! RemixがSPAモードのサポートを開始しました!! CSR作りたいなら素のReactよりRemix採用したほうが良いかもよ!!

remix.run