問題
現在のプロジェクトでは、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
それでは、abbreviations
とnames
は元々{: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
iex
でMyApp.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
}
]
本当に欲しかったとはabbreviation
とname
という仮フィール名でしたが、embed_one
で定義されてるフィールドを:virtual
に指定することは許可されていないようです。今の結果で単一のabbreviations
とnames
を配列で囲まないことで、技術的にスキーマを破っています。とりあえずいいでしょう。
デフォルトの言語
とにかく、それは一種の仕事をします。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),
これは英語では機能しましたが、日本語では、英語と日本語のabbreviations
とnames
の組み合わせごとに%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つやり方があります:
-
WITH ORDINALITY
を使用して生のSQLを実行し、可能性を[lang, :en, Removal]
順位でソートして、利用可能な最初のものを選択する。 -
各結果を処理して
%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{}
をそれぞれ処理が必要がありますが、それらを新しく作成する代わりに、abbreviations
とnames
を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")
を呼び出すと、すべてのリーグが英語のabbreviations
とnames
だけで返されます。これはまさに私が望むように機能しています。
結論
PostgresのJSONBオブジェクトにXQueryと同じようにアクセスするには、Postgresの拡張機能について多くを学ぶ必要があると予想していました。でも、データベースレヤーの代わりにExistで結局のところ、コンテキスト内でデータをフィルタリングする方が実際には簡単です。
まだまだ道のりは長く、データ構造の多くの再編成が行われます。しかし、このプロジェクトにとって非常に有望なスタートです。ここではPostgresの特別なJSONB機能を利用しないが、将来的には便利になると確信しています。