LoginSignup
18
3

More than 1 year has passed since last update.

問題

現在のプロジェクトでは、XMLデータベースをPostgresに移植しています。しかし、過去 15年間、徐々にリレーショナルデータベースから離れていきました。ネイティブXMLデータベースが、従来のリレーショナルデータベースよりも多次元にわたって情報をまとめる優れた方法であることと思ってます。この間に構築された情報システムは、MLBチームが購読しているアジアの野球選手に関する情報を提供するだけでなく、ファンタジースポーツサイトにも力を与えています。

XQueryは関数型プログラミング言語であるので、Elixirへの移行は自然に感じられました。しかし、データベースの部分は後回しにしていました。さまざまなEctoの本を読んでおり、Postgresと共に、Ecto の埋め込みJSONB blobを処理できることを知っています。しかし、実際にデータを移植して、現在XQueryで楽しんでいるのと同じように埋め込みデータに簡単にアクセスできるでしょうか?

概念実証を構築しましょう。

シンプルに始める

簡単なことから始めましょう。リーグ名に関する多言語メタデータ情報を含むリーグのXML文書(レコード)をいくつか持っています。人間の名前はより複雑であるため、リーグは開始するのに適した場所です。

まず、リーグのXMLドキュメントがどのようなものかを見てみましょう。

<?xml version="1.0" encoding="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="Nippon Professional Baseball"/>
        <y:name lang="ja" shortname="プロ野球" fullname="一般社団法人日本野球機構" shortname-ruby="ぷろやきゅう" fullname-ruby="いっぱんしゃだんほうじんにっぽんやきゅうきこう"/>
    </y:league-metadata>
</y:league>

リーグの略語も欲しいですので、それも埋め込みマップとして追加しましょう。

$ mix phx.gen.live Sports League leagues sd_id:string abbreviations:array:map names:array:map

以上のsd_id は、XML データベース内のリーグ ID への参照です。 データはゆっくりと Postgres データベースに移行されるため、着信 XML データを新しいリレーショナル データに関連する必要があります。 確かに、「識別子に意味はない」というルールを破ったことはわかっていますが、XMLドキュメントを手動で作成するときに、人間にとって使いやすいものにすることは非常に便利でした。

とにかく、生成されたMyApp.SportsコンテキストとMyApp.Sports.League データベーススキーマを微調整しました。他のテーブルにも埋め込まれた多言語の略語と名前を使用したかったので、それらのために個別の埋め込みスキーマを作成しました。

略語 (abbreviations)

略語は非常に簡単です。

lib/my_app/sports/abbr_embed.ex

defmodule Jbc.Sports.AbbrEmbed do
  import Ecto.Changeset
  use Ecto.Schema

  @langs ~w(en ja ko zh_cn zh_tw es)a

  @type t :: %__MODULE__{
    id: Ecto.UUID.t(),
    lang: atom,
    abbr: String.t(),
    abbr_ruby: [String.t() | nil]
  }

  embedded_schema do
    field :lang, Ecto.Enum, values: @langs
    field :abbr, :string
    field :abbr_ruby, :string
  end

  @doc false
  def changeset(abbr, attrs) do
    abbr
    |> cast(attrs, [:lang, :abbr, :abbr_ruby])
    |> validate_required([:lang, :abbr])
  end
end

ルビーとは?

LaTeX などのDTP ソフトウェアでは「ルビ」行があります。たとえば、「漢字」の上に「かんじ」を出力するために、次のように記述します。

\usepackage{okumacro}
...
\ruby{漢字}{かんじ}

名前 (names)

名前は、略語よりもわずかに詳細です。

lib/my_app/sports/name_embed.ex

defmodule Jbc.Sports.NameEmbed do
  import Ecto.Changeset
  use Ecto.Schema

  @langs ~w(en ja ko zh_cn zh_tw es)a

  @type t :: %__MODULE__{
    id: Ecto.UUID.t(),
    lang: atom,
    name: String.t(),
    full_name: [String.t() | nil],
    name_ruby: [String.t() | nil],
    full_name_ruby: [String.t() | nil]
  }

  embedded_schema do
    field :lang, Ecto.Enum, values: @langs
    field :name, :string
    field :full_name, :string
    field :name_ruby, :string
    field :full_name_ruby, :string
  end

  @doc false
  def changeset(name, attrs) do
    name
    |> cast(attrs, [:lang, :name, :full_name, :name_ruby, :full_name_ruby])
    |> validate_required([:lang, :name])
  end
end

ここには通称とフルネームがあり、どちらにもオプションのよみがな(ルビ)フィールドがあります。必須フィールドは :lang:name だけです。

これは2回目「@langs」のリストを利用しているので、きっと最後ではないと思います。確かに共通モジュールに引っ張り出すべきだと思われます。 とりあえず、このままでいいでしょう。

リーグ

ここで注目している主なテーブルである %League{} に到達します。

lib/my_app/sports/league.ex

defmodule Jbc.Sports.League do
  use Ecto.Schema
  import Ecto.Changeset
  alias Jbc.Sports.{AbbrEmbed, NameEmbed}

  @type t :: %__MODULE__{
    id: integer,
    sd_id: String.t(),

    abbreviations: AbbrEmbed.t(),
    names: NameEmbed.t(),

    inserted_at: NaiveDateTime.t(),
    updated_at: NaiveDateTime.t()
  }

  schema "leagues" do
    field :sd_id, :string

    embeds_many :abbreviations, AbbrEmbed
    embeds_many :names, NameEmbed

    timestamps()
  end

  @doc false
  def changeset(league, attrs) do
    league
    |> cast(attrs, [:sd_id])
    |> cast_embed(:abbreviations)
    |> cast_embed(:names)
    |> validate_required([:sd_id])
    |> unique_constraint(:sd_id)
  end
end

これは、「embeds_many」で埋め込みの「略語」と「名前」を宣言した場所です。on_replaceにデフォルトの:raiseオプションを使用します。これによる影響はまだわかりませんが、これから学んでいきます。更新時にすべてのオプションを削除して再挿入する必要があることを少し心配しています。

移行 (migration)

上記のmixマンドでマイグレーションが自動生成されました。

priv/repo/migrations/20221117075336_create_leagues.exs

defmodule MyApp.Repo.Migrations.CreateLeagues do
  use Ecto.Migration

  def change do
    create table(:leagues) do
      add :sd_id, :string
      add :abbreviations, :map
      add :names, :map

      timestamps()
    end

    create unique_index(:leagues, [:sd_id])
    execute "CREATE INDEX league_abbr_index ON leagues USING GIN ((abbreviations -> 'lang'))"
    execute "CREATE INDEX league_name_index ON leagues USING GIN ((names -> 'lang'))"
  end
end

それでは、abbreviationsnamesは元々{:array, :map}で定義されました。しかし、ドキュメント(HexとElixirフォーラムの投稿)に基づいて、両方とも:map型で定義することにしました。

私のXMLデータベースでは、言語に基づいてクエリを作成することが多いため、「lang」フィールドをインデックスとして持つことは必須です。リーグ名もインデックスするかどうかを後で考えてみます。

リーグを取得する際の主要なインデックスはsd_idを使えます。

データベースにリーグを入力する

このテーブルをいじるためには、いくつかのデータレコードが必要です。生成されたSportsコンテキストを使用する簡単なスクリプトで実行できます。

/priv/repo/league_seeds.exs

%{
  sd_id: "npb",
  abbreviations: [
    %{lang: "en", abbr: "NPB"},
    %{lang: "ja", abbr: "NPB", abbr_ruby: "エンピービー"}
  ],
  names: [
    %{lang: "en", name: "NPB", full_name: "Nippon Professional Baseball"},
    %{lang: "ja", name: "プロ野球", full_name: "一般社団法人日本野球機構", name_ruby: "エンピービー", full_name_ruby: "いっぱんしゃだんほうじんにっぽんやきゅうきこう"}
  ]
}
|> MyApp.Sports.create_league()

%{
  sd_id: "kbo",
  abbreviations: [
    %{lang: "en", abbr: "KBO"},
    %{lang: "ja", abbr: "KBOリーグ", abbr_ruby: "ケービーオーリーグ"},
    %{lang: "ko", abbr: "KBO 리그"}
  ],
  names: [
    %{lang: "en", name: "KBO League", full_name: "Korea Baseball Organization"},
    %{lang: "ja", name: "KBOリーグ", full_name: "韓国野球委員会", name_ruby: "ケービーオーリーグ", full_name_ruby: "かんこくやきゅういいんかい"},
    %{lang: "ko", name: "KBO 리그", full_name: "한국야구위원회"}
  ]
}
|> MyApp.Sports.create_league()

%{
  sd_id: "cpbl",
  abbreviations: [
    %{lang: "en", abbr: "CPBL"},
    %{lang: "ja", abbr: "台湾プロ野球", abbr_ruby: "たいわんプロやきゅう"},
    %{lang: "zh_tw", abbr: "中華職棒"}
  ],
  names: [
    %{lang: "en", name: "CPBL", full_name: "Chinese Professional Baseball League"},
    %{lang: "ja", name: "中華職棒", full_name: "中華職業棒球大聯盟", name_ruby: "たいわんプロやきゅう", full_name_ruby: "うかしょくぎょうぼうきゅうだいれんめい"},
    %{lang: "zh_tw", name: "中華職棒", full_name: "中華職業棒球大聯盟"}
  ]
}
|> MyApp.Sports.create_league()

すべてのリーグが同じ言語をサポートしているわけではないことに注意してください。私が重点的に取り組んでいる主要な2つの言語は、英語と日本語です。しかし、韓国と台湾の野球を扱うときは、母国語の名前が手元にあるのが大事と思ってます。

$ mix run priv/repo/league_seeds.exs

以上のコマンドを実行すると、操作するデータがいくつかあるはずです。

iexでは、簡単なMyApp.Sports.list_leagues()が次を返します:

[
  %Jbc.Sports.League{
    __meta__: #Ecto.Schema.Metadata<:loaded, "leagues">,
    id: 1,
    sd_id: "npb",
    abbreviations: [
      %Jbc.Sports.AbbrEmbed{
        id: "d233c86b-da71-4b9b-998e-c33c85dee9cb",
        lang: :en,
        abbr: "NPB",
        abbr_ruby: nil
      },
      %Jbc.Sports.AbbrEmbed{
        id: "c095b6c0-5128-458b-bdde-660219f23bd7",
        lang: :ja,
        abbr: "NPB",
        abbr_ruby: "エンピービー"
      }
    ],
    names: [
      %Jbc.Sports.NameEmbed{
        id: "dda5f844-e46d-48fc-9796-70d35839174f",
        lang: :en,
        name: "NPB",
        full_name: "Nippon Professional Baseball",
        name_ruby: nil,
        full_name_ruby: nil
      },
      %Jbc.Sports.NameEmbed{
        id: "ab2394e1-1f2b-48bc-8b58-228e2a2d0247",
        lang: :ja,
        name: "プロ野球",
        full_name: "一般社団法人日本野球機構",
        name_ruby: "エンピービー",
        full_name_ruby: "いっぱんしゃだんほうじんにっぽんやきゅうきこう"
      }
    ],
    inserted_at: ~N[2022-11-23 09:21:22],
    updated_at: ~N[2022-11-23 09:21:22]
  },
  %Jbc.Sports.League{
    __meta__: #Ecto.Schema.Metadata<:loaded, "leagues">,
    id: 2,
    sd_id: "kbo",
    abbreviations: [
      %Jbc.Sports.AbbrEmbed{
        id: "3feee1e1-3410-4b4c-b70e-ae9b3d0524f3",
        lang: :en,
        abbr: "KBO",
        abbr_ruby: nil
      },
      %Jbc.Sports.AbbrEmbed{
        id: "0cd5718d-4ae4-43db-9138-8e36cd918e41",
        lang: :ja,
        abbr: "KBOリーグ",
        abbr_ruby: "ケービーオーリーグ"
      },
      %Jbc.Sports.AbbrEmbed{
        id: "4017287c-88d9-4c31-8b78-6558bcf78437",
        lang: :ko,
        abbr: "KBO 리그",
        abbr_ruby: nil
      }
    ],
    ...
  }
  ...
]

出力が長いなので、切りました。でも、3 つのリーグすべてに2つまたは3つの言語の「略語」と「名前」が含まれています。

1つの言語のみのリーグをクエリーする

これは、多くの時間と研究を要すると予想していた部分です。そして、それはしました。何度か失敗した後、最終的にmanhtranlihn による このElixirフォーラムの投稿でわかってきました。まず、データベース側で言語をフィルタリングする方法のコードを見てみましょう。これをSportsコンテキストに入ります。

  def list_leagues_in_lang(lang) do
    from(l in League,
      inner_join: n in fragment("jsonb_array_elements(?)", l.names),
      inner_join: a in fragment("jsonb_array_elements(?)", l.abbreviations),
      where: fragment("?->>'lang' = ?", a, ^lang),
      where: fragment("?->>'lang' = ?", n, ^lang),
      select: [l.id, l.sd_id, n.value, a.value]
    )
    |> Repo.all()
  end

iexMyApp.Sports.list_leagues_in_lang("en")を呼び出します。

[
  [
    1,
    "npb",
    %{
      "abbr" => "NPB",
      "abbr_ruby" => nil,
      "id" => "d233c86b-da71-4b9b-998e-c33c85dee9cb",
      "lang" => "en"
    },
    %{
      "full_name" => "Nippon Professional Baseball",
      "full_name_ruby" => nil,
      "id" => "dda5f844-e46d-48fc-9796-70d35839174f",
      "lang" => "en",
      "name" => "NPB",
      "name_ruby" => nil
    }
  ],
  [
    2,
    "kbo",
    %{
      "abbr" => "KBO",
      "abbr_ruby" => nil,
      "id" => "3feee1e1-3410-4b4c-b70e-ae9b3d0524f3",
      "lang" => "en"
    },
    %{
      "full_name" => "Korea Baseball Organization",
      "full_name_ruby" => nil,
      "id" => "924c7beb-96ad-44d6-86f5-62966e810ecb",
      "lang" => "en",
      "name" => "KBO League",
      "name_ruby" => nil
    }
  ],
  [
    3,
    "cpbl",
    %{
      "abbr" => "CPBL",
      "abbr_ruby" => nil,
      "id" => "9f05c47c-f38e-4cb9-a4e3-2f75b9c8a049",
      "lang" => "en"
    },
    %{
      "full_name" => "Chinese Professional Baseball League",
      "full_name_ruby" => nil,
      "id" => "ae5a0a4e-936b-4707-89ad-c14e32ea2518",
      "lang" => "en",
      "name" => "CPBL",
      "name_ruby" => nil
    }
  ]
]

うーん。行いましたが、期待していたものとはまったく異なります。%League{}構造体のリストの代わりに、値の配列のリストを取得します。キー名を紛失しました。

配列されたレコードのリストを調べてレコードを%League{}構造体に変換する処理を追加しましょう。list_leagues_in_lang関数を次のように更新します。

  def list_leagues_in_lang(lang) do
    from(l in League,
      inner_join: a in fragment("jsonb_array_elements(?)", l.abbreviations),
      inner_join: n in fragment("jsonb_array_elements(?)", l.names),
      where: fragment("?->>'lang' = ?", a, ^lang),
      where: fragment("?->>'lang' = ?", n, ^lang),
      select: [l.id, l.sd_id, a.value, n.value]
    )
    |> Repo.all()
    |> Enum.into([], fn l ->
      [id, sd_id, abbreviation, name] = l
      %MyApp.Sports.League{id: id, sd_id: sd_id, abbreviations: abbreviation, names: name}
    end)
  end

じゃ、MyApp.Sports.list_leagues_in_lang("en")を実行すると、最初に期待していたものが得られます。

[
  %MyApp.Sports.League{
    __meta__: #Ecto.Schema.Metadata<:built, "leagues">,
    id: 1,
    sd_id: "npb",
    abbreviations: %{
      "abbr" => "NPB",
      "abbr_ruby" => nil,
      "id" => "d233c86b-da71-4b9b-998e-c33c85dee9cb",
      "lang" => "en"
    },
    names: %{
      "full_name" => "Nippon Professional Baseball",
      "full_name_ruby" => nil,
      "id" => "dda5f844-e46d-48fc-9796-70d35839174f",
      "lang" => "en",
      "name" => "NPB",
      "name_ruby" => nil
    },
    inserted_at: nil,
    updated_at: nil
  },
  %MyApp.Sports.League{
    __meta__: #Ecto.Schema.Metadata<:built, "leagues">,
    id: 2,
    sd_id: "kbo",
    abbreviations: %{
      "abbr" => "KBO",
      "abbr_ruby" => nil,
      "id" => "3feee1e1-3410-4b4c-b70e-ae9b3d0524f3",
      "lang" => "en"
    },
    names: %{
      "full_name" => "Korea Baseball Organization",
      "full_name_ruby" => nil,
      "id" => "924c7beb-96ad-44d6-86f5-62966e810ecb",
      "lang" => "en",
      "name" => "KBO League",
      "name_ruby" => nil
    },
    inserted_at: nil,
    updated_at: nil
  },
  %MyApp.Sports.League{
    __meta__: #Ecto.Schema.Metadata<:built, "leagues">,
    id: 3,
    sd_id: "cpbl",
    abbreviations: %{
      "abbr" => "CPBL",
      "abbr_ruby" => nil,
      "id" => "9f05c47c-f38e-4cb9-a4e3-2f75b9c8a049",
      "lang" => "en"
    },
    names: %{
      "full_name" => "Chinese Professional Baseball League",
      "full_name_ruby" => nil,
      "id" => "ae5a0a4e-936b-4707-89ad-c14e32ea2518",
      "lang" => "en",
      "name" => "CPBL",
      "name_ruby" => nil
    },
    inserted_at: nil,
    updated_at: nil
  }
]

本当に欲しかったとはabbreviationnameという仮フィール名でしたが、embed_oneで定義されてるフィールドを:virtualに指定することは許可されていないようです。今の結果で単一のabbreviationsnamesを配列で囲まないことで、技術的にスキーマを破っています。とりあえずいいでしょう。

デフォルトの言語

とにかく、それは一種の仕事をします。MyApp.Sports.list_leagues_in_lang("ja")を実行すると、日本語の略語と名前で3つのリーグが取得されます。ただし、MyApp.Sports.list_leagues_in_lang("ko")は韓国野球委員会(KBO)のみを返します。

[
  %MyApp.Sports.League{
    __meta__: #Ecto.Schema.Metadata<:built, "leagues">,
    id: 2,
    sd_id: "kbo",
    abbreviations: %{
      "abbr" => "KBO 리그",
      "abbr_ruby" => nil,
      "id" => "4017287c-88d9-4c31-8b78-6558bcf78437",
      "lang" => "ko"
    },
    names: %{
      "full_name" => "한국야구위원회",
      "full_name_ruby" => nil,
      "id" => "58eeffb8-280a-4ebc-a555-082441a84af7",
      "lang" => "ko",
      "name" => "KBO 리그",
      "name_ruby" => nil
    },
    inserted_at: nil,
    updated_at: nil
  }
]

もちろん、そうですよ!WHEREの条件が行っていることです。XQuery では、言語固有の部分を抽出するためのサブクエリはルート レベルをフィルタリングしません。

それより、XQueryで行っていることは、指定された言語のリソースが存在しない場合、デフォルトで英語を返すということです。それも存在しない場合は、最初の要素が何であれ返します。

最初に考えたのは、各略語と名前のWHEREフラグメントを次のように展開することでした。

      where: fragment("?->>'lang' = ? or ?->>'lang' = 'en'", a, ^lang, a),
      where: fragment("?->>'lang' = ? or ?->>'lang' = 'en'", n, ^lang, n),

これは英語では機能しましたが、日本語では、英語と日本語のabbreviationsnamesの組み合わせごとに%League{}を取得しました。韓国語と台湾語の場合は少し混沌としていませんでしたが、それでも望んでいたものではありませんでした.

PostgresのWITH ORDINALITY機能をやるを調べることでした。これはPostgresの生のSQLで機能しましたが、Ectoは機能しないフラグメントにASを追加しようとし続けました。WITH ORDINALITYの後にオプションのASが続き、さらに table_alias (column_alias, ordinal_alias)が続きます。Ecto は、この3つのエイリアスの処理方法をまだ知らないようです。この方法は将来有望に見えますが、今はそうではありません。

言語によるフィルター

Postgresの機能を活用するのではなく、Elixir側で言語によってフィルター処理することの方がいいかも知りません。結局、潜在的な6つの言語のみを扱っています:

@langs ~w(en ja ko zh_cn zh_tw es)a

2つやり方があります:

  1. WITH ORDINALITYを使用して生のSQLを実行し、可能性を[lang, :en, Removal]順位でソートして、利用可能な最初のものを選択する。

  2. 各結果を処理して%League{}構造体の場合、各言語固有のフィールドでEnum.sort() |> Enum.at()を使用してElixir側で同じ優先ソート処理を実行する。

2番の方がほど複雑ではない解決策になる可能性があることに気づきました。

まず、langキーを持つマップ(スキーマ構造体を含む)のリストからアイテムを取得する関数を書きましょう。

  def get_lang_item(items, lang) do
    items
    |> Enum.sort(fn a, b -> rank_lang(a, lang) <= rank_lang(b, lang) end)
    |> Enum.at(0)
  end

  defp rank_lang(record, lang) when is_binary(lang), do: rank_lang(record, String.to_atom(lang))
  defp rank_lang(%{lang: value} = _record, lang) when is_atom(lang) and value == lang, do: 0
  defp rank_lang(%{lang: value} = _record, _lang) when value == :en, do: 1
  defp rank_lang(_record, _lang), do: 2

これを機能させるための鍵は、並べ替えアルゴリズムを好みでランク付けすることです。langが存在する場合は、0の値でランクを付けされます。英語のレコード(langでない場合)は、1 にランクを付けされます。それ以外はすべて2で均等にランクを付けされます。 Enum.sort/2はランクされたレコードを:asc順 (a <= b) でソートします。次に、ランク付けされた並べ替えの最初のレコードが返されます。

rank_lang/2の最初のパターンマッチは、文字列のlangパラメータをアトムに変換します。データベースからの結果のlangフィールドがアトムにキャストされて、上記のデフォルトのlist_leagues/0関数でRepo.all(League)を直接呼び出して取得した戻り値に反映されます。内部的には、アトムで list_leagues_in_lang(lang)を呼び出しますが、後でフォームを扱うため、文字列から変換することを考えなくて済むようにするとよいでしょう。

そしてlist_leagues_in_lang/1といえば、これが新しいバージョンです:

  def list_leagues_in_lang(lang) do
    Repo.all(League)
    |> Enum.into([], fn l ->
      %{l |
        abbreviations: [get_lang_item(l.abbreviations, lang)],
        names: [get_lang_item(l.names, lang)]
    }
    end)
  end

それはとても素敵に見えます。

返された%League{}をそれぞれ処理が必要がありますが、それらを新しく作成する代わりに、abbreviationsnamesを1つにフィルター処理された言語JSONレコードに置き換えます(依然として配列にラップされて)。

それでは、テストしてみましょう。

MyApp.Sports.list_leagues_in_lang(:ja)を実行すると:

[
  %MyApp.Sports.League{
    __meta__: #Ecto.Schema.Metadata<:loaded, "leagues">,
    id: 1,
    sd_id: "npb",
    abbreviations: [
      %MyApp.Sports.AbbrEmbed{
        id: "c095b6c0-5128-458b-bdde-660219f23bd7",
        lang: :ja,
        abbr: "NPB",
        abbr_ruby: "エンピービー"
      }
    ],
    names: [
      %MyApp.Sports.NameEmbed{
        id: "ab2394e1-1f2b-48bc-8b58-228e2a2d0247",
        lang: :ja,
        name: "プロ野球",
        full_name: "一般社団法人日本野球機構",
        name_ruby: "エンピービー",
        full_name_ruby: "いっぱんしゃだんほうじんにっぽんやきゅうきこう"
      }
    ],
    inserted_at: ~N[2022-11-23 09:21:22],
    updated_at: ~N[2022-11-23 09:21:22]
  },
  %MyApp.Sports.League{
    __meta__: #Ecto.Schema.Metadata<:loaded, "leagues">,
    id: 2,
    sd_id: "kbo",
    abbreviations: [
      %MyApp.Sports.AbbrEmbed{
        id: "0cd5718d-4ae4-43db-9138-8e36cd918e41",
        lang: :ja,
        abbr: "KBOリーグ",
        abbr_ruby: "ケービーオーリーグ"
      }
    ],
    names: [
      %MyApp.Sports.NameEmbed{
        id: "5abd32ad-1976-4661-8ba2-c7eaa885cb67",
        lang: :ja,
        name: "KBOリーグ",
        full_name: "韓国野球委員会",
        name_ruby: "ケービーオーリーグ",
        full_name_ruby: "かんこくやきゅういいんかい"
      }
    ],
    inserted_at: ~N[2022-11-23 09:21:22],
    updated_at: ~N[2022-11-23 09:21:22]
  },
  %MyApp.Sports.League{
    __meta__: #Ecto.Schema.Metadata<:loaded, "leagues">,
    id: 3,
    sd_id: "cpbl",
    abbreviations: [
      %MyApp.Sports.AbbrEmbed{
        id: "65bf65b2-c827-48b1-99a5-f29772045c87",
        lang: :ja,
        abbr: "台湾プロ野球",
        abbr_ruby: "たいわんプロやきゅう"
      }
    ],
    names: [
      %MyApp.Sports.NameEmbed{
        id: "7123f315-fab2-4e3e-b4c9-6281bd3fabd3",
        lang: :ja,
        name: "中華職棒",
        full_name: "中華職業棒球大聯盟",
        name_ruby: "たいわんプロやきゅう",
        full_name_ruby: "うかしょくぎょうぼうきゅうだいれんめい"
      }
    ],
    inserted_at: ~N[2022-11-23 09:21:22],
    updated_at: ~N[2022-11-23 09:21:22]
  }
]

完全だよ!では、:koはどうか?

[
  %MyApp.Sports.League{
    __meta__: #Ecto.Schema.Metadata<:loaded, "leagues">,
    id: 1,
    sd_id: "npb",
    abbreviations: [
      %MyApp.Sports.AbbrEmbed{
        id: "d233c86b-da71-4b9b-998e-c33c85dee9cb",
        lang: :en,
        abbr: "NPB",
        abbr_ruby: nil
      }
    ],
    names: [
      %MyApp.Sports.NameEmbed{
        id: "dda5f844-e46d-48fc-9796-70d35839174f",
        lang: :en,
        name: "NPB",
        full_name: "Nippon Professional Baseball",
        name_ruby: nil,
        full_name_ruby: nil
      }
    ],
    inserted_at: ~N[2022-11-23 09:21:22],
    updated_at: ~N[2022-11-23 09:21:22]
  },
  %MyApp.Sports.League{
    __meta__: #Ecto.Schema.Metadata<:loaded, "leagues">,
    id: 2,
    sd_id: "kbo",
    abbreviations: [
      %MyApp.Sports.AbbrEmbed{
        id: "4017287c-88d9-4c31-8b78-6558bcf78437",
        lang: :ko,
        abbr: "KBO 리그",
        abbr_ruby: nil
      }
    ],
    names: [
      %MyApp.Sports.NameEmbed{
        id: "58eeffb8-280a-4ebc-a555-082441a84af7",
        lang: :ko,
        name: "KBO 리그",
        full_name: "한국야구위원회",
        name_ruby: nil,
        full_name_ruby: nil
      }
    ],
    inserted_at: ~N[2022-11-23 09:21:22],
    updated_at: ~N[2022-11-23 09:21:22]
  },
  %MyApp.Sports.League{
    __meta__: #Ecto.Schema.Metadata<:loaded, "leagues">,
    id: 3,
    sd_id: "cpbl",
    abbreviations: [
      %MyApp.Sports.AbbrEmbed{
        id: "9f05c47c-f38e-4cb9-a4e3-2f75b9c8a049",
        lang: :en,
        abbr: "CPBL",
        abbr_ruby: nil
      }
    ],
    names: [
      %MyApp.Sports.NameEmbed{
        id: "ae5a0a4e-936b-4707-89ad-c14e32ea2518",
        lang: :en,
        name: "CPBL",
        full_name: "Chinese Professional Baseball League",
        name_ruby: nil,
        full_name_ruby: nil
      }
    ],
    inserted_at: ~N[2022-11-23 09:21:22],
    updated_at: ~N[2022-11-23 09:21:22]
  }
]

KBOのabbreviationsとnamesの両方を韓国語で表記しているが、他のリーグはデフォルトの英語を返した。また、MyApp.Sports.list_leagues_in_lang("ko")で呼び出すと、:ko`と同じ結果が返されます。

最後に、MyApp.Sports.list_leagues_in_lang("invalid")を呼び出すと、すべてのリーグが英語のabbreviationsnamesだけで返されます。これはまさに私が望むように機能しています。

結論

PostgresのJSONBオブジェクトにXQueryと同じようにアクセスするには、Postgresの拡張機能について多くを学ぶ必要があると予想していました。でも、データベースレヤーの代わりにExistで結局のところ、コンテキスト内でデータをフィルタリングする方が実際には簡単です。

まだまだ道のりは長く、データ構造の多くの再編成が行われます。しかし、このプロジェクトにとって非常に有望なスタートです。ここではPostgresの特別なJSONB機能を利用しないが、将来的には便利になると確信しています。

18
3
0

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
18
3