Ashで多言語レコード管理

## 問題

解決すべき問題は、ネイティブXMLデータベースからリレーショナルデータベース(特に Postgres)に移植するプロジェクトを再開することです。XMLデータベースの最も気に入っている点は、一部のデータを異なるテーブルに分割するのではなく、関連する情報を 1つの「ドキュメント」に簡単にまとめることができることです。

たとえば、私のスポーツデータベースには多数のリーグがあります。リーグにはすべて名前があって、名前は複数の言語 (英語、日本語、韓国語、中国語)で保持しています。すべてのレコードにすべての言語のエントリがあるわけではありませんが、特定のリーグをクエリすると、特定の言語で名前が取得されることを期待します。その言語の名前がない場合は、日本語で取得します。それもない場合は、利用可能な最初の言語でリーグ名を取得します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/4833bacf-db76-94bf-b711-0123fa037aec.png)

Postgresには、JSONデータの「フラグメント(「`embedded`」データ)をフィールドに保存し、クエリを実行する機能があります。これにより、XMLデータの多次元性が維持されます。しかし、そのデータにアクセスするには、覚えにくいし、非標準のSQL文が必要で、インターネット上のサンプルは非常に少ないです。

数年前に[この投稿](https://qiita.com/westbaystars/items/13d0ae9079c903ba604f)で、Ectoで実行する方法をようやく理解しました。Ashフレームワーク(データベースを定義する宣言的な利用する方法)を使用すると、Ectoより適切に処理できるかどうかを確認したいと思います。

## 新規プロジェクトを作成

Elixir 開発環境がすでにあると想定しています。最終的にはWebアプリケーションになるので、新しいPhoenixアプリを初期化しましょう:

```bash
mix phx.new my_app && cd my_app
```

`deps`関係を取得してインストールします。

`mix.exs`の`deps`リストに`:ash`ではなくて、Ashの`:igniter`を追加しましょう。

```elixir
    {:igniter, "~> 0.3"}
```

Ashの`igniter`は、構成やその他の定型ファイルに必要な変更を自動的に行う一連の`mix`タスクであり、変更を行う前に常に承認を求めます。例えば、`Ecto.Repo`を利用する`Application.Repo`に`AshPostgres.Repo`を使うように変更することです。

じゃ、Igniterを取得してコンパイルします。

```bash
mix deps.get
mix deps.compile
```

次に、Igniterを利用して、`:spark`、`:ash`、`:ash_postgres`をプロジェクトに追加します。

```bash
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の拡張機能をインストールしてください:

```elixir
  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
<?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`ファイルを追加します。

```elixir
# 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

名前は人間以外の名前です。これがリーグ、チーム、などに利用するつもりです。定義を見てみましょう:

```elixir
# /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`で利用するモジュールに指定の代わりに独自形で指定しています:

```elixir
# 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`(選手)などのスキーマを配置するコンテキストであると考えています。

```elixir
# 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`を作成するには、次を実行します。

```bash
mix ash_postgres.generate_migrations --name add_league
```

あらっ!エラーになりました。

```bash
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`ブロックの後に以下を追加しましょう:

```elixir
  config :my_app,
    ash_domains: [MyApp.Sports]
```

もう一度マイグレーションを実行すると:

```bash
% 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つ目はリーグのテーブルを作成します。

マイグレーションを行いましょう。

```bash
mix ash_postgres.migrate
```

これがいつくの関数を作成して、拡張子をインストールして、`leagues`テーブルを作成します。

## リーグを追加して、取得する

テーブルが出来上がって、リーグデータを挿入しましょう。Ashで`action`を指定して、できるようになります。`lib/my_app/sports/league.ex`ファイルに以下を追加してください:

```elixir
  actions do
    create :create do
      accept [:sid, :names]
    end

    defaults [:read]
  end
```

`:create`のアクションはデータを挿入するためのもので、`:read`は`leagues`テーブルからデータを取得するためのアクションです。挿入の場合、`:sid`と`:names`を渡すことができます。

以下のリーグデータを`seeds.exs`に入れてください:

```elixir
# 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 つのレコードがデータベースに挿入されます。別のターミナルで、次のことを確認できます。

```bash
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`を別のターミナルで起動して:

```elixir
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`を追加します:

```elixir
# 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`モジュールで行われます。詳しく見てみましょう:

```elixir
# 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`セッションに:

```elixir
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]`の代わりに以下でロードしましょう。

```elixir
# 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`しましょう。

```elixir
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})`でクエリーすることが必要です。

```elixir
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">,
    ...
  >
]
```

パラメータがないと同じですね。じゃ、英語で取得しましょう。

```elixir
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`でパラメータを渡さなげばいけません。

```elixir
# 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
```

もう一度英語で取得してみろう。

```elixir
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`方法ができて、フィルターを含まれましょう:

```elixir
# 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
    ...
```

台湾のプロ野球リーグを中国語で取得してみましょう:

```elixir
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でこのプロジェクトを前進させるだろうと思います。