6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

問題

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

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

image.png

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

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

新規プロジェクトを作成

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

mix phx.new my_app && cd my_app

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

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

    {:igniter, "~> 0.3"}

Ashのigniterは、構成やその他の定型ファイルに必要な変更を自動的に行う一連のmixタスクであり、変更を行う前に常に承認を求めます。例えば、Ecto.Repoを利用するApplication.RepoAshPostgres.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.EnumEcto.Enumとちょっと違います。Ectoで定義するとき、アトムと整数に関連することができます。Ashの場合、アドムと人間がわかる言葉に関連しています。これでアプリのプルダウンまたはドキュメンテーションに生成する可能になって、便利です。

Sportsドメイン

最後に、テーブルに入れることができるすべてのリソースはAsh.Domainに入れる必要があります。これは、Leagueだけではなくて、TeamPerson(選手)などのスキーマを配置するコンテキストであると考えています。

# 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しかありません。NameLangは内部の形ですので、ここに指定しません。

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.exsconfig :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-osspcitextの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のアクションはデータを挿入するためのもので、:readleaguesテーブルからデータを取得するためのアクションです。挿入の場合、: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.Calculationbehaviourを実装します。そのため、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でこのプロジェクトを前進させるだろうと思います。

6
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?