プログラミング
Biff.graph: Clojureコードベースをクエリ可能なグラフとして構造化する
Biff.graph: structure your Clojure codebase as a queryable graph (github.com)
要約
Biff.graphは、Clojureコードベースのデータモデルをクエリ可能なグラフとして構造化するためのライブラリです。データベースとビジネスロジック/派生データを統合されたグラフとして照会でき、コードの理解とテストを容易にします。これはPathomの軽量版として位置づけられ、よりシンプルなアプローチでグラフデータモデリングパターンを提供します。
全文翻訳
biff.graph データモデルをクエリ可能なグラフとして構造化します。biff.graph を使用すると、データベースとビジネスロジック/派生データの両方を、統一された拡張可能なグラフとしてクエリできます。データモデルのコードは、小さく独立したチャンク(「リゾルバ」)に分割でき、アプリケーションコードは、そのデータをどのように取得するかを知る必要なしに、必要なデータの形状を宣言できます。これにより、コードベースは、特に大きくなるにつれて、理解しやすくテストしやすくなります。
biff.graph は基本的に Pathom の軽量版です。Pathom の機能の一部のみを実装しており、理解しやすくすることを意図しています。最大の С違いは、biff.graph にはクエリ計画ステップがないため、一部のクエリを Pathom ほど効率的に実行できない可能性があることです。(ただし、バッチリゾルバとキャッシュはサポートしています。)一方で、biff.graph のコードベースは約 600 行で、クエリ実行コードは約 200 行です。
私が biff.graph を作成したのは、仕事でもサイドプロジェクトでも Pathom を使うのが好きでしたが、デフォルトで Biff に含めることには懸念があったからです。まだ Pathom を聞いたこともない小規模プロジェクトに取り組んでいる人々にとって、コード構造の利点が、必要な追加学習コストを上回るとは確信が持てません。そのため、biff.graph は、このグラフデータモデリングパターンをどれだけ親しみやすくできるかを探る試みです。biff.graph を使い続ける Biff ユーザーは、コードの構造化に関して Pathom と同じ概念を学ぶ必要がありますが、次のステップである、コードが実際に何をしているのかを理解して問題発生時にデバッグできるようになることは、より簡単になるはずです。
依存関係
com.biffweb/graph {:mvn/version "2.0.0-rc7"}
ステータス
このライブラリは、他のすべての Biff 2 ライブラリがリリースされるまでリリース候補版となります。それまでは破壊的な変更がある可能性がありますが、私はそれを予期していません。
API リファレンス
com.biffweb.graph
コンセプト
biff.graph は、データの形状を記述するために、EQL / Datomic のプルパターンをわずかに変更したサブセットを使用します。「リゾルバ」は、関連付けられた入力クエリと出力クエリを持つ関数です。関数は、入力クエリの形状のデータを受け取り、出力クエリの形状のデータを返します。多くのリゾルバを定義した後、biff.graph のクエリエンジンはそれらを使用して、クエリする形状のデータを返します。
例
このスニペットでは、指定された主キーに対してエンティティを返す、2 つのシミュレートされたデータベースアクセスリゾルバ(rss-feed と post)を定義しています。絵文字をフィルタリングしたバージョンを返す、派生データリゾルバ(clean-post-title)を定義しています。データベースから取得される属性と派生される属性のどちらを知る必要もなく、com.biffweb.graph/query を介してデータモデルグラフをクエリします。
(require '[com.biffweb.graph :as biff.graph :refer [defresolver]])
(require '[clojure.string :as str])
(defresolver rss-feed {:input [:rss-feed/id] :output [:rss-feed/title]} [_ctx {:rss-feed/keys [id]}]
(get {1 {:rss-feed/title "My Blog"}} id))
(defresolver post {:input [:post/id] :output [:post/title :post/url {:post/rss-feed [:rss-feed/id]}]} [_ctx {:post/keys [id]}]
(get {2 {:post/url "https://example.com/my-post" :post/title "My Post 🎅" :post/rss-feed {:rss-feed/id 1}}
3 {:post/title "My Other Post 🎅" :post/rss-feed {:rss-feed/id 1}}}
id))
(defn remove-emojis [s]
(str/replace s #"🎅" ""))
(defresolver clean-post-title {:input [:post/title] :output [:post/clean-title]} [_ctx {:post/keys [title]}]
{:post/clean-title (-> title remove-emojis str/trim)})
(def resolvers [rss-feed post clean-post-title])
(def ctx (biff.graph/new-ctx resolvers))
(biff.graph/query ctx
[{:post/id 2} {:post/id 3}]
[:post/id :post/clean-title ;; Optional attributes are denoted with [:? ...]
[:? :post/url]
;; Join attributes are denoted with nested maps
{:post/rss-feed [:rss-feed/title]}
;; An optional join attribute looks like this:
;; {[:? :post/rss-feed] [:rss-feed/title]}
])
;; => [{:post/id 2, :post/clean-title "My Post", :post/url "https://example.com/my-post", :post/rss-feed {:rss-feed/title "My Blog"}}
;; {:post/id 3, :post/clean-title "My Other Post", :post/rss-feed {:rss-feed/title "My Blog"}}]
使用法
リゾルバの定義
まず、データベーススキーマに基づいてリゾルバを自動生成するような関数(defresolver ではなく com.biffweb.graph/resolver を使用)を用意するのが一般的です(上記の例の post と rss-feed は自動生成されるでしょう)。
ガイドライン:
テーブル / エンティティタイプごとに 1 つのリゾルバが必要です。入力クエリは主キーのみ(例: [:person/id])であるべきです。出力クエリには、他のすべての列 / 属性(例: [:person/age, :person/favorite-color, ...])を含めるべきです。出力クエリには、エンティティ内の各外部キー / 参照属性の結合キーも含まれるべきで、結合サブクエリはそのエンティティの主キーであるべきです(例: [{:person/pet [:pet/id]}, ...])。リゾルバオプションには :batch true を含めるべきです。これにより、データベースクエリで複数のエンティティを一度にフェッチできます。この設定を持つリゾルバは、入力マップのベクトルを受け取り、同じ順序で出力マップのベクトルを返す必要があります。リゾルバ関数は、基本的に各入力主キーに対して SELECT * を実行し、結合キーが存在し、ネストされたマップとしてフォーマットされていることを確認する必要があります。まだリリースされていない biff.sqlite ライブラリには、sqlite 用のそのような関数が含まれています(コンテンツ警告:編集されていない AI コード)。その後、必要に応じて com.biffweb.graph/defresolver を使用して、役立つと思われる追加のリゾルバを定義できます。必要に応じて、ヘルパー関数からリゾルバにロジックを徐々に移動できます。リゾルバの書き方の詳細については、リファレンスドキュメントを参照してください。
最後に、リゾルバを com.biffweb.graph/new-ctx に渡します。これは、クエリエンジンが必要とする簡単なインデックス作成を行います。new-ctx は、キャッシュや検証などを処理するミドルウェアでリゾルバをラップします。
クエリの実行
リゾルバを定義した後、Ring リクエストハンドラの開始時など、必要な場所からクエリを実行できます。
(defn settings-page [{:keys [session] :as ctx}]
(let [user (biff.graph/query ctx {:user/id (:uid session)} [:user/email :user/display-name :user/subscribed])]
...))
上記の例では、com.biffweb.graph/new-ctx の出力を受信 Ring リクエストにマージするミドルウェアがあると仮定しています。
デバッグ
biff.graph/query 内からスローされた例外には、例外データに :biff.graph/trace キーが含まれています。これは、例外が発生したときにグラフ走査におけるクエリエンジンの場所(例外がどのクエリ部分から発生したか、どのリゾルバの入力を解決しようとしていたかなど)を示します。必要に応じて、失敗したパスにクエリを集中させることで、最小限の再現例を作成するためにこの情報を使用できます。
テスト
リゾルバ関数は :biff.graph/resolve-fn キーの下に格納されます。これらは単一の引数(ctx マップ)を取り、リゾルバの入力は :biff.graph/input の下に格納されていることを期待します。
(defresolver my-resolver {:input [:user/id] :output [...]} [ctx input] ...)
(deftest test-my-resolver
(is (= ((:biff.graph/resolve-fn my-resolver) {:biff.graph/input {:user/id 1}}) ...)))
biff.fx 統合
biff.fx を使用する場合、com.biffweb.graph/fx-handlers をハンドラマップにマージできます。これは :biff.graph.fx/query として com.biffweb.graph/query を公開します。
(require '[com.biffweb.fx :refer [defmachine]])
(defmachine do-something :start
(fn [{:keys [session]}]
{:user [:biff.graph.fx/query {:user/id (:uid session)} [:user/email ...]]
:biff.fx/next :next})
...)
リゾルバ内でエフェクト(データベースクエリや外部サービス呼び出しなど)を処理するために biff.fx を使用したい場合、defresolver はリゾルバ本体を biff.fx マシンとして定義する形式もサポートしています。
(defresolver my-resolver {:input [...] :output [...]} :start (fn [ctx input] ...) :next (fn [ctx input] ...))
注:defresolver で定義された状態関数は、通常の biff.fx マシンとは異なり、1 つではなく 2 つのパラメータを取ります。
biff.core 統合