問題
解決すべき問題は、ネイティブXMLデータベースからリレーショナルデータベース(特に Postgres)に移植するプロジェクトを再開することです。XMLデータベースの最も気に入っている点は、一部のデータを異なるテーブルに分割するのではなく、関連する情報を 1つの「ドキュメント」に簡単にまとめることができることです。
たとえば、私のスポーツデータベースには多数のリーグがあります。リーグにはすべて名前があって、名前は複数の言語 (英語、日本語、韓国語、中国語)で保持しています。すべてのレコードにすべての言語のエントリがあるわけではありませんが、特定のリーグをクエリすると、特定の言語で名前が取得されることを期待します。その言語の名前がない場合は、日本語で取得します。それもない場合は、利用可能な最初の言語でリーグ名を取得します。
Postgresには、JSONデータの「フラグメント(「embedded
」データ)をフィールドに保存し、クエリを実行する機能があります。これにより、XMLデータの多次元性が維持されます。しかし、そのデータにアクセスするには、覚えにくいし、非標準のSQL文が必要で、インターネット上のサンプルは非常に少ないです。
数年前にこの投稿で、Ectoで実行する方法をようやく理解しました。Ashフレームワーク(データベースを定義する宣言的な利用する方法)を使用すると、Ectoより適切に処理できるかどうかを確認したいと思います。
新規プロジェクトを作成
Elixir 開発環境がすでにあると想定しています。最終的にはWebアプリケーションになるので、新しいPhoenixアプリを初期化しましょう:
mix phx.new my_app && cd my_app
deps
関係を取得してインストールします。
mix.exs
のdeps
リストに:ash
ではなくて、Ashの:igniter
を追加しましょう。
{:igniter, "~> 0.3"}
Ashのigniter
は、構成やその他の定型ファイルに必要な変更を自動的に行う一連のmix
タスクであり、変更を行う前に常に承認を求めます。例えば、Ecto.Repo
を利用するApplication.Repo
にAshPostgres.Repo
を使うように変更することです。
じゃ、Igniterを取得してコンパイルします。
mix deps.get
mix deps.compile
次に、Igniterを利用して、:spark
、:ash
、:ash_postgres
をプロジェクトに追加します。
mix igniter.install spark
mix igniter.install ash
mix igniter.install ash_postgres
:ash_postgres
インストールでは、使用している Postgres の最も低いバージョン (major.minor.patch) を入力するように求められます。バージョン 16.0.0
より前バージョンの場合は、入力してください。
最後の準備は、lib/my_app_repo.ex
にAshの拡張機能をインストールしてください:
def installed_extensions do
# Add extensions here, and the migration generator will install them.
["ash-functions", "uuid-ossp", "citext"]
end
- "
uuid-ossp
"は:uuid_v7
サポートをPostgresに追加します -
citext
は文字列の大文字・小文字別ではないようなタイプをPostgresに追加します
最初のマイグレーションを作成して行う時に適用になります。
リーグのテープルを作ろう
準備されたので、簡単なものから始めましょう。リーグ名に関する多言語メタデータ情報を含むリーグ「ドキュメント」(レコード)をいくつか持っています。
まず、リーグのXMLドキュメントがどのようなものかを見てみましょう。
<?xml version="1.0"coding="UTF-8"?>
<y:league xmlns:y="http://scoutdragon.com/schema/yakyu-ml.xsd" id="npb">
<y:league-metadata>
<y:name lang="en" shortname="NPB" fullname="日本プロ野球"/>
<y:name lang="ja" shortname="プロ野球" fullname="一般社団法人日本野球機構" by="ぷろやきゅう" fullname-ruby="いっぱんしゃだんほうじんにっぽんやきゅうきこう"/>
</y:league-metadata>
</y:league>
通常、phx.gen.live を組み合わせてデータベースとページの CRUD セットと Ecto コンテキストを初期化します。Ash では、データの外観を宣言することだけに焦点を当てます。ファイル構造は、共通のコンテキストディレクトリの下にすべてのデータ宣言があるという点でEctoと似ています。扱っているデータはすべて「スポーツ」(SportsMLスキーマを利用)に関連しているので、コンテキストをsports
と呼び、リーグを定義するためのleague.ex
ファイルを追加します。
# lib/my_app/sports/league.ex
defmodule MyApp.Sports.League do
use Ash.Resource,
domain: MyApp.Sports,
data_layer: AshPostgres.DataLayer
alias MyApp.Sports.Name
attributes do
attribute :id, :integer do
writable? false
generated? true
primary_key? true
allow_nil? false
end
attribute :sid, :ci_string do
allow_nil? false
public? true
end
attribute :names, {:array, Name}, public?: true
attribute :inserted_at, :utc_datetime_usec do
writable? false
default &DateTime.utc_now/0
match_other_defaults? true
allow_nil? false
end
attribute :updated_at, :utc_datetime_usec do
writable? false
default &DateTime.utc_now/0
update_default &DateTime.utc_now/0
match_other_defaults? true
allow_nil? false
end
end
postgres do
table "leagues"
repo MyApp.Repo
end
end
このテーブルには 5つのフィールドがあります:
-
:id
— 自動生成された整数 -
:sid
— 人間が読める IDとして大文字と小文字を区別しない文字列 -
:names
— 埋め込まれた(JSON)名の配列 -
:inserted_at
— レコードが挿入されたときのタイムスタンプ -
:updated_at
— レコードが最後に更新されたときのタイムスタンプ
Ectoスキーマを設定すると、:id
、:inserted_at
、および :updated_at
フィールドはすべて推測されます。Ashのすべてを定義する方法の方が好きです。:id
を UUIDにすることを考えましたが、最終的には連続した整数にすることにしました。リーグの次のID番号が何になるかを推測しても害はありません。その以上に、手動でレコードを呼び出したい場合があるので、UUIDを入力するより簡単です。
:name
は埋め込みスキーマであり、個別に定義する必要があります。
Name
名前は人間以外の名前です。これがリーグ、チーム、などに利用するつもりです。定義を見てみましょう:
# /lib/my_app/sports/name.ex
defmodule MyApp.Sports.Name do
use Ash.Resource,
data_layer: :embedded,
embed_nil_values?: false
validations do
validate present([:lang, :common_name, :full_name])
end
attributes do
attribute :lang, MyAppdefmodule MyApp.Sports.Name do
use Ash.Resource,
data_layer: :embedded,
embed_nil_values?: false
validations do
validate present([:lang, :common_name, :full_name])
end
attributes do
attribute :lang, MyApp.Sports.Lang do
allow_nil? false
default :ja
end
attribute :common_name, :ci_string do
public? true
allow_nil? false
end
attribute :full_name, :ci_string do
public? true
allow_nil? false
end
attribute :common_name_ruby, :string do
public? true
allow_nil? true
end
attribute :full_name_ruby, :string do
public? true
allow_nil? true
end
end
end
名前にあるファール度は:
-
:lang
- 言語コード (以下に参照) -
:common_name
- 通称 -
:full_name
- フルネーム -
:common_name_ruby
- 通称のよみがな -
:full_name_ruby
- フルネームのよみがな
「
ruby
」は LaTeXのよみがな項目名です。例えば「かんじ」を「漢字」の上(又は縦書きの右)に書く時に「\ruby{漢字}{かんじ}
」で書きました。そのネーミングを続いて使っています。_ruby
は日本語の名前のみ使用されます。
Lang
言語をいろんなテーブルに使いますから、@langs ~w(en ja ko zh_cn zh_tw)a
で利用するモジュールに指定の代わりに独自形で指定しています:
# lib/my_app/sports.lang.ex
defmodule MyApp.Sports.Lang do
use Ash.Type.Enum, values: [
en: "English",
ja: "日本語",
ko: "한국인",
zh_cn: "简体中文",
zh_tw: "繁體中文"
]
end
Ash.Type.Enum
はEcto.Enum
とちょっと違います。Ectoで定義するとき、アトムと整数に関連することができます。Ashの場合、アドムと人間がわかる言葉に関連しています。これでアプリのプルダウンまたはドキュメンテーションに生成する可能になって、便利です。
Sportsドメイン
最後に、テーブルに入れることができるすべてのリソースはAsh.Domain
に入れる必要があります。これは、League
だけではなくて、Team
やPerson
(選手)などのスキーマを配置するコンテキストであると考えています。
# lib/my_app/sports/sports.ex
defmodule MyApp.Sports do
use Ash.Domain
resources do
resource MyApp.Sports.League
# resource MyApp.Sports.Team # 未定義
# resource MyApp.Sports.Person # 未定義
end
end
ドメインで指定するのは、リソースをリストすることだけです。現在は MyApp.Sports.League
しかありません。Name
やLang
は内部の形ですので、ここに指定しません。
Migration
データベースのテーブルが定義されたので、テーブルを作成するために必要な移行(migration)を作成しましょう。config/dev.exs
の必要なMyApp.Repo
設定が設定されていることをしましたでしょう。
migration
を作成するには、次を実行します。
mix ash_postgres.generate_migrations --name add_league
あらっ!エラーになりました。
warning: No domains found, so no resource-related migrations will be generated.
Pass the `--domains` option or configure `config :your_app, ash_domains: [...]`
...
そうですね。ignitor
はコンフィグファイルを修正した時にMyApp.Sports
のドメインが存在されませんでした。config/config.exs
にconfig :ash
ブロックの後に以下を追加しましょう:
config :my_app,
ash_domains: [MyApp.Sports]
もう一度マイグレーションを実行すると:
% mix ash_postgres.generate_migrations --name add_league
Compiling 19 files (.ex)
...
Generated my_app app
Extension Migrations:
* creating priv/resource_snapshots/repo/extensions.json
* creating priv/repo/migrations/20241008050225_install_2_extensions.exs
Generating Tenant Migrations:
Generating Migrations:
* creating priv/repo/migrations/20241008050226_add_league.exs
2つマイグレーションを作成しました。1つ目は uuid-ossp
とcitext
のPostgres拡張する形です。2つ目はリーグのテーブルを作成します。
マイグレーションを行いましょう。
mix ash_postgres.migrate
これがいつくの関数を作成して、拡張子をインストールして、leagues
テーブルを作成します。
リーグを追加して、取得する
テーブルが出来上がって、リーグデータを挿入しましょう。Ashでaction
を指定して、できるようになります。lib/my_app/sports/league.ex
ファイルに以下を追加してください:
actions do
create :create do
accept [:sid, :names]
end
defaults [:read]
end
:create
のアクションはデータを挿入するためのもので、:read
はleagues
テーブルからデータを取得するためのアクションです。挿入の場合、:sid
と:names
を渡すことができます。
以下のリーグデータをseeds.exs
に入れてください:
# priv/repo/seeds.exs
alias MyApp.Sports.{League, Name}
League
|> Ash.Changeset.for_create(
:create, %{
sid: "npb",
names: [
%Name{lang: "en", common_name: "NPB", full_name: "Nippon Professional Baseball"},
%Name{lang: "ja", common_name: "プロ野球", full_name: "一般社団法人日本野球機構",
common_name_ruby: "エンピービー", full_name_ruby: "いっぱんしゃだんほうじんにっぽんやきゅうきこう"}
]
}
)
|> Ash.create!()
League
|> Ash.Changeset.for_create(
:create, %{
sid: "kbo",
names: [
%Name{lang: "en", common_name: "KBO League", full_name: "Korea Baseball Organization"},
%Name{lang: "ja", common_name: "KBOリーグ", full_name: "韓国野球委員会",
common_name_ruby: "ケービーオーリーグ", full_name_ruby: "かんこくやきゅういいんかい"},
%Name{lang: "ko", common_name: "KBO 리그", full_name: "한국야구위원회"}
]
}
)
|> Ash.create!()
League
|> Ash.Changeset.for_create(
:create, %{
sid: "cpbl",
names: [
%Name{lang: "en", common_name: "CPBL", full_name: "Chinese Professional Baseball League"},
%Name{lang: "ja", common_name: "中華職棒", full_name: "中華職業棒球大聯盟",
common_name_ruby: "たいわんプロやきゅう", full_name_ruby: "うかしょくぎょうぼうきゅうだいれんめい"},
%Name{lang: "zh_tw", common_name: "中華職棒", full_name: "中華職業棒球大聯盟"}
]
}
)
|> Ash.create!()
NPB、KBO、CPBLの情報をマップに入れて、Ash.Changeset
を作成します。embed
されてるName
構造体配列も含まれてます。これが大事です。エンベッドする構造体が「%Name{...}」で指定しなければいけません。テーブルの内容が普通のマップ(
%{...}`)で渡します。
mix run priv/repo/seeds.exs
を実行すると、3 つのレコードがデータベースに挿入されます。別のターミナルで、次のことを確認できます。
psql my_app_dev
select * from leagues;
id | sid | names | inserted_at | updated_at
----+------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+----------------------------
1 | npb | {"{\"lang\": \"en\", \"full_name\": \"Nippon Professional Baseball\", \"common_name\": \"NPB\"}","{\"lang\": \"ja\", \"full_name\": \"一般社団法人日本野球機構\", \"common_name\": \"プロ野球\", \"full_name_ruby\": \"いっぱんしゃだんほうじんにっぽんやきゅうきこう\", \"common_name_ruby\": \"エンピービー\"}"} | 2024-10-08 08:34:18.556482 | 2024-10-08 08:34:18.556482
2 | kbo | {"{\"lang\": \"en\", \"full_name\": \"Korea Baseball Organization\", \"common_name\": \"KBO League\"}","{\"lang\": \"ja\", \"full_name\": \"韓国野球委員会\", \"common_name\": \"KBOリーグ\", \"full_name_ruby\": \"かんこくやきゅういいんかい\", \"common_name_ruby\": \"ケービーオーリーグ\"}","{\"lang\": \"ko\", \"full_name\": \"한국야구위원회\", \"common_name\": \"KBO 리그\"}"} | 2024-10-08 08:34:18.587883 | 2024-10-08 08:34:18.587883
3 | cpbl | {"{\"lang\": \"en\", \"full_name\": \"Chinese Professional Baseball League\", \"common_name\": \"CPBL\"}","{\"lang\": \"ja\", \"full_name\": \"中華職業棒球大聯盟\", \"common_name\": \"中華職棒\", \"full_name_ruby\": \"うかしょくぎょうぼうきゅうだいれんめい\", \"common_name_ruby\": \"たいわんプロやきゅう\"}","{\"lang\": \"zh_tw\", \"full_name\": \"中華職業棒球大聯盟\", \"common_name\": \"中華職棒\"}"} | 2024-10-08 08:34:18.589466 | 2024-10-08 08:34:18.589466
(3 rows)
日本語以外のサブレコードでは、名前に_ruby
フィールドが含まれていないことに注目してください。これはembedded_nil_values?: false
が機能しているということです。この機能が大好きです!
では、リーグを取得する方法は何でしょうか? プロジェクトのルートにiex -S mix
を別のターミナルで起動して:
alias MyApp.Sports.{Lang, Name, League} # Handy aliases
require Ash.Query # For filter and other macros
League |> Ash.Query.filter(sid == "npb") |> Ash.read!()
# 結果
[debug] QUERY OK source="leagues" db=21.3ms decode=0.6ms queue=1.8ms idle=1337.7ms
SELECT l0."id", l0."names", l0."sid", l0."inserted_at", l0."updated_at" FROM "leagues" AS l0 WHERE (l0."sid"::citext = $1::citext) [#Ash.CiString<"npb">]
↳ anonymous fn/3 in AshPostgres.DataLayer.run_query/2, at: lib/data_layer.ex:771
[
#MyApp.Sports.League<
__meta__: #Ecto.Schema.Metadata<:loaded, "leagues">,
id: 1,
sid: #Ash.CiString<"npb">,
names: [
#MyApp.Sports.Name<
__meta__: #Ecto.Schema.Metadata<:built, "">,
lang: :en,
common_name: #Ash.CiString<"NPB">,
full_name: #Ash.CiString<"Nippon Professional Baseball">,
common_name_ruby: nil,
full_name_ruby: nil,
aggregates: %{},
calculations: %{},
...
>,
#MyApp.Sports.Name<
__meta__: #Ecto.Schema.Metadata<:built, "">,
lang: :ja,
common_name: #Ash.CiString<"プロ野球">,
full_name: #Ash.CiString<"一般社団法人日本野球機構">,
common_name_ruby: "エンピービー",
full_name_ruby: "いっぱんしゃだんほうじんにっぽんやきゅうきこう",
aggregates: %{},
calculations: %{},
...
>
],
inserted_at: ~U[2024-10-08 08:34:18.556482Z],
updated_at: ~U[2024-10-08 08:34:18.556482Z],
aggregates: %{},
calculations: %{},
...
>
]
簡単です! 以前にJSONフィールドをEctoで取得することが大変難しいでした。これでは本当に簡単です。しかも、大事なところで魔法なことがないです。
指定言語のリーグ名
実際に全部のリーグ名がほしくないで、指定されてる言語のリソースをWebページに表示したいです。しかし、指定されてる言語がない場合、日本語がデフォルトになってほしいです。日本語も無ければ、最初にある言語でを戻します。
まず、calculations
(計算)ブロックの下にcalculation
を追加します:
# lib/my_app/sports/league.exe
...
alias MyApp.Sports.{Name, Lang} # Langを追加する
alias MyApp.Calculations.LangEmbeddedJSON
...
calculations do
calculate :name,
Name,
{LangEmbeddedJSON, field: :names} do
argument :lang, Lang do
allow_nil? false
default :ja
end
end
end
:name
の値を「計算」するには、:names
フィールドを利用して、:lang
という引数が必要です(指定されていない場合は:lang
のデフォルトは:ja
になります)。
実際の「計算」はMyApp.Calculations.LangEmbeddedJSON
モジュールで行われます。詳しく見てみましょう:
# lib/my_app/calculations/lang_embedded_resource.ex
defmodule MyApp.Calculations.LangEmbeddedJSON do
use Ash.Resource.Calculation
@default_lang :ja
@impl true
def init(opts) do
if opts[:field] && is_atom(opts[:field]) do
{:ok, opts}
else
{:error, "言語のエンベッド配列 :fieldを指定するが必要"}
end
end
@impl true
def load(_query, opts, _context) do
opts[:field]
end
@impl true
def calculate(records, [field: field], %{arguments: %{lang: lang}}) do
Enum.map(records, fn record -> get_field_for_lang(record, field, lang) end)
end
# エンベッドされてるレコードが以下の順番に探す:
# * 指定された言語
# * 日本語 (デフォルト言語)
# * 最初の項目
defp get_field_for_lang(record, field, lang) do
items = Map.get(record, field)
item = Enum.find(items, fn embed -> embed.lang == lang end)
cond do
item == nil and lang == @default_lang -> List.first(items)
item == nil -> Enum.find(items, List.first(items), fn embed -> embed.lang == @default_lang end)
true -> item
end
end
end
これはAsh.Resource.Calculation
のbehaviour
を実装します。そのため、init(opts)
、load(query, opts, context)
、calculate(records, opts, context)
をインプリメント(実装)します。
init(opts)
は、フィールド名がアトムとして取得されたことを確認します。フィールド名が実際に存在するかどうかは確認しません。ただし、必要なパラメーターが取得されたことは少なくとも確認します。
load(_query, opts, _context)
は、opts
で渡されたフィールド名の値を返すだけです。
calculate(records, [field: field], %{arguments: %{lang: lang}})
で実の処理が行われます。関数シグネチャでパターンマッチングを使用して、指定されたフィールド名と言語の両方を抽出します。関数は、すべてのレコードをループし、次の条件に基づいて埋め込まれたフラグメントを返します:
- 指定された言語
- 日本語 (4行目に指定されたデフォルト言語)
- 最初に見つかった項目
エンベッドされた略語がある項目は、それが返されます。この「計算」は、:lang
属性を持つ任意のJSONB
フラグメントで使用できます。例えば、未来にPersonName
も同じCalculation
を使えます。
これまで試してみましょう。iex
セッションに:
recompile
League |> Ash.Query.filter(sid == "npb") |> Ash.read!()
# 結果
[debug] QUERY OK source="leagues" db=25.2ms idle=1721.6ms
SELECT l0."id", l0."names", l0."sid", l0."inserted_at", l0."updated_at" FROM "leagues" AS l0 WHERE (l0."sid"::citext = $1::citext) [#Ash.CiString<"npb">]
↳ anonymous fn/3 in AshPostgres.DataLayer.run_query/2, at: lib/data_layer.ex:771
[
#MyApp.Sports.League<
name: #Ash.NotLoaded<:calculation, field: :name>,
...
...
>
]
惜しい。名前の「計算」をロードしませんでした。じゃ、defaults [:read]
の代わりに以下でロードしましょう。
# lib/my_app/sports/league.ex
...
read :read do
prepare build(load: [:name])
primary? true
argument :lang, Lang do
allow_nil? false
default :ja
end
end
...
もう一度リコンパイルして、:read
しましょう。
recompile
# 結果
League |> Ash.Query.filter(sid == "npb") |> Ash.read!()
[debug] QUERY OK source="leagues" db=1.2ms idle=1380.0ms
SELECT l0."id", l0."names", l0."sid", l0."inserted_at", l0."updated_at" FROM "leagues" AS l0 WHERE (l0."sid"::citext = $1::citext) [#Ash.CiString<"npb">]
↳ anonymous fn/3 in AshPostgres.DataLayer.run_query/2, at: lib/data_layer.ex:771
[
#MyApp.Sports.League<
name: #MyApp.Sports.Name<
__meta__: #Ecto.Schema.Metadata<:built, "">,
lang: :ja,
common_name: #Ash.CiString<"プロ野球">,
full_name: #Ash.CiString<"一般社団法人日本野球機構">,
common_name_ruby: "エンピービー",
full_name_ruby: "いっぱんしゃだんほうじんにっぽんやきゅうきこう",
aggregates: %{},
calculations: %{},
...
>,
__meta__: #Ecto.Schema.Metadata<:loaded, "leagues">,
id: 1,
sid: #Ash.CiString<"npb">,
...
>
]
できました!
パラメータを渡すためにAsh.Query.for_read(:read, %{lang: :ja})
でクエリーすることが必要です。
League |> Ash.Query.for_read(:read, %{lang: :ja}) |> Ash.Query.filter(sid == "npb") |> Ash.read!()
# 結果
[debug] QUERY OK source="leagues" db=5.9ms decode=0.4ms idle=1681.8ms
SELECT l0."id", l0."names", l0."sid", l0."inserted_at", l0."updated_at" FROM "leagues" AS l0 WHERE (l0."sid"::citext = $1::citext) [#Ash.CiString<"npb">]
↳ anonymous fn/3 in AshPostgres.DataLayer.run_query/2, at: lib/data_layer.ex:771
[
#MyApp.Sports.League<
name: #MyApp.Sports.Name<
__meta__: #Ecto.Schema.Metadata<:built, "">,
lang: :ja,
common_name: #Ash.CiString<"プロ野球">,
full_name: #Ash.CiString<"一般社団法人日本野球機構">,
common_name_ruby: "エンピービー",
full_name_ruby: "いっぱんしゃだんほうじんにっぽんやきゅうきこう",
aggregates: %{},
calculations: %{},
...
>,
__meta__: #Ecto.Schema.Metadata<:loaded, "leagues">,
id: 1,
sid: #Ash.CiString<"npb">,
...
>
]
パラメータがないと同じですね。じゃ、英語で取得しましょう。
League |> Ash.Query.for_read(:read, %{lang: :en}) |> Ash.Query.filter(sid == "npb") |> Ash.read!()
# 結果
[debug] QUERY OK source="leagues" db=1.1ms idle=1892.2ms
SELECT l0."id", l0."names", l0."sid", l0."inserted_at", l0."updated_at" FROM "leagues" AS l0 WHERE (l0."sid"::citext = $1::citext) [#Ash.CiString<"npb">]
↳ anonymous fn/3 in AshPostgres.DataLayer.run_query/2, at: lib/data_layer.ex:771
[
#MyApp.Sports.League<
name: #MyApp.Sports.Name<
__meta__: #Ecto.Schema.Metadata<:built, "">,
lang: :ja,
common_name: #Ash.CiString<"プロ野球">,
full_name: #Ash.CiString<"一般社団法人日本野球機構">,
common_name_ruby: "エンピービー",
full_name_ruby: "いっぱんしゃだんほうじんにっぽんやきゅうきこう",
aggregates: %{},
calculations: %{},
...
>,
__meta__: #Ecto.Schema.Metadata<:loaded, "leagues">,
id: 1,
sid: #Ash.CiString<"npb">,
...
>
]
これも日本語だ! どうしたの?
あっ、prepare
でパラメータを渡さなげばいけません。
# lib/my_app/sports/league.ex
...
read :read do
prepare build(load: [name: expr(%{lang: ^arg(:lang)})]) # <- ここ
primary? true
argument :lang, Lang do
allow_nil? false
default :ja
end
end
もう一度英語で取得してみろう。
recompile()
League |> Ash.Query.for_read(:read, %{lang: :en}) |> Ash.Query.filter(sid == "npb") |> Ash.read!()
# 結果
[debug] QUERY OK source="leagues" db=1.0ms idle=1396.6ms
SELECT l0."id", l0."names", l0."sid", l0."inserted_at", l0."updated_at" FROM "leagues" AS l0 WHERE (l0."sid"::citext = $1::citext) [#Ash.CiString<"npb">]
↳ anonymous fn/3 in AshPostgres.DataLayer.run_query/2, at: lib/data_layer.ex:771
[
#MyApp.Sports.League<
name: #MyApp.Sports.Name<
__meta__: #Ecto.Schema.Metadata<:built, "">,
lang: :en,
common_name: #Ash.CiString<"NPB">,
full_name: #Ash.CiString<"Nippon Professional Baseball">,
common_name_ruby: nil,
full_name_ruby: nil,
aggregates: %{},
calculations: %{},
...
>,
__meta__: #Ecto.Schema.Metadata<:loaded, "leagues">,
id: 1,
sid: #Ash.CiString<"npb">,
...
>
]
NPBの韓国語では League |> Ash.Query.for_read(:read, %{lang: :ko}) |> Ash.Query.filter(sid == "npb") |> Ash.read!()
日本語が戻します。
KBOの韓国語名は League |> Ash.Query.for_read(:read, %{lang: :ko}) |> Ash.Query.filter(sid == "kbo") |> Ash.read!()
は韓国語で戻します。
直接にリーグと言語で読み込む
以上のパイプラインがちょっと長いです。Ashで複数 Read
方法ができて、フィルターを含まれましょう:
# lib/my_app/sports/league.ex
...
read :by_league do
prepare build(load: [name: expr(%{lang: ^arg(:lang)})])
filter expr(sid == ^arg(:sid))
get? true
argument :sid, :string do
allow_nil? false
end
argument :lang, Lang do
allow_nil? false
default :en
end
end
...
台湾のプロ野球リーグを中国語で取得してみましょう:
recompile
League |> Ash.Query.for_read(:by_league, %{sid: "cpbl", lang: :zh_tw}) |> Ash.read_one!()
# 結果
[debug] QUERY OK source="leagues" db=1.4ms idle=1050.4ms
SELECT l0."id", l0."names", l0."sid", l0."inserted_at", l0."updated_at" FROM "leagues" AS l0 WHERE (l0."sid"::citext = $1::citext) [#Ash.CiString<"cpbl">]
↳ anonymous fn/3 in AshPostgres.DataLayer.run_query/2, at: lib/data_layer.ex:771
#MyApp.Sports.League<
name: #MyApp.Sports.Name<
__meta__: #Ecto.Schema.Metadata<:built, "">,
lang: :zh_tw,
common_name: #Ash.CiString<"中華職棒">,
full_name: #Ash.CiString<"中華職業棒球大聯盟">,
common_name_ruby: nil,
full_name_ruby: nil,
aggregates: %{},
calculations: %{},
...
>,
__meta__: #Ecto.Schema.Metadata<:loaded, "leagues">,
id: 3,
sid: #Ash.CiString<"cpbl">,
...
>
できました! Ashはなかなかいいですね!
次のステップはSports
コンテキストにヘルパー関数を書きます。でも、このような例が英語でも、どこにも、見たことないです。Ectoでこれをするとはもっと大変だと思います。やっと、Ashでこのプロジェクトを前進させるだろうと思います。