如何在 Ecto 2.0 中通过 JSON 属性在 Repo.preload 中执行自定义 select 查询?

How to perform in Ecto 2.0 custom select query in Repo.preload by JSON attribute?

我有 Post 类别和许多标签,标签和类别包含 hstore name 文件,其中包含键中的翻译。

如何通过纯 Ecto 查询预加载相关的 post.tags 和 select JSON tag.name 例如 "pl" 键?在这种情况下,我想将区域设置键 "pl" 作为参数传递,但我不知道如何插入它,没有任何效果。

defmodule Myapp.Tag do
  use Myapp.Web, :model

  schema "tags" do
    field :name, :map

    has_many :taggings, Myapp.Tagging
    has_many :posts, through: [:taggings, :post]

    belongs_to :post, Myapp.Post
    timestamps
  end
end

defmodule Myapp.Tagging do
  use Myapp.Web, :model

  schema "taggings" do
    belongs_to :post, Myapp.Post
    belongs_to :tag, Myapp.Tag
  end
end

defmodule Myapp.Post do
  use Myapp.Web, :model

  schema "posts" do
    field :title, :string

    belongs_to :category, Myapp.Category
    has_many :taggings, Myapp.Tagging
    has_many :tags, through: [:taggings, :tag]
    timestamps
  end
end

测试:

post = Repo.get!(Post, id) |> Repo.preload([:tags, :category])
post.tags # => "tags":[{"name":{"pl":"narty","en":"ski"}},{"name":{"pl":"wspinaczka","en":"climbing"}}]

#how preload all with selected key?

posts = Post 
  |> Repo.all 
  |> Repo.preload(tags: from(t in Myapp.Tag, select: fragment("?::json->?", t.name, "pl")))

错误:

** (BadMapError) expected a map, got: "narty"
    (stdlib) :maps.find(:id, "narty")
    (elixir) lib/map.ex:27: Map.fetch!/2
    (elixir) lib/enum.ex:1387: Enum."-reduce/3-lists^foldl/2-0-"/3
    (elixir) lib/enum.ex:1043: anonymous fn/3 in Enum.map/2
    (elixir) lib/enum.ex:1387: Enum."-reduce/3-lists^foldl/2-0-"/3
    (elixir) lib/enum.ex:1043: Enum.map/2
    (elixir) lib/enum.ex:1043: anonymous fn/3 in Enum.map/2
    (elixir) lib/enum.ex:1387: Enum."-reduce/3-lists^foldl/2-0-"/3
    (elixir) lib/enum.ex:1043: Enum.map/2
    (stdlib) erl_eval.erl:669: :erl_eval.do_apply/6
    (stdlib) erl_eval.erl:438: :erl_eval.expr/5
       (iex) lib/iex/evaluator.ex:117: IEx.Evaluator.handle_eval/5

我愿意:

post.tags # => "tags":["narty","wspinaczka"]

迁移:

defmodule Myapp.Repo.Migrations.CreateTag do
  use Ecto.Migration

  def change do
    create table(:tags) do
      add :name, :map
      add :category_id, references(:categories)

      timestamps
    end

  end
end

defmodule Myapp.Repo.Migrations.CreateJoinTableTaggings do
  use Ecto.Migration

  def change do
    create table(:taggings) do
      add :post_id, references(:posts)
      add :tag_id, references(:tags)
    end
    create index(:taggings, [:post_id, :tag_id])
  end
end

更新

Ecto 2.0 支持预加载查询中的自定义 select 字段,所以这是答案:

|> Repo.preload(tags: from(t in Myapp.Tag, select: %{id: t.id, data: fragment("?::json->?", t.name, "pl")}))

一种方法是使用 Enum.map 函数过滤标签。

Repo.get!(Post, id) |> Repo.preload([:tags])
|> Map.get(:tags)
|> Enum.map(&(&1.name.pl))

这是一样的,只是语法不同:

Repo.get!(Post, id) |> Repo.preload([:tags])
|> Map.get(:tags)
|> Enum.map(fn(tag) -> tag.name.pl end)

Returns 一个列表 ["narty", "wspinaczka"]

另一种方法是使用 Ecto.Query 模块并使用 Ecto 查询。

尽管它与 Repo.preload 方法有点不同,所以我不确定它是否正是您要找的。

基于之前的many-one数据模型:

query = from(from t in MyApp.Tag, where: t.post_id == ^id, select: t)
Repo.all(query) |> Enum.map(&(&1.name.pl))

根据编辑后的 ​​many-many 数据模型,这应该更接近:

post_id = "some_id" # <-- enter your id here
query = Ecto.Query.from tagging in MyApp.Tagging,
        join: tag in assoc(tagging, :tags),
        where: tagging.post_id == ^post_id,
        select: tag
Repo.all(query) |> Enum.map(&(&1.name.pl))

你能按照这些思路尝试一下吗... 我不确定它是否有效,因为我无法 运行 它,可能需要修改 json 片段查询..

Repo.all(
  from tagging in MyApp.Tagging,
  join: tag in assoc(tagging, :tags), 
  where: tagging.post_id == ^post_id, 
  select: fragment("?::json#>'{?,?}'", tag, ^"name", ^"pl")
) 

不确定您是否可以覆盖 Ecto.Association(即):tags,但是您可以过滤预加载并映射值。

Repo.get!(Post, id) |> Repo.preload(tags: from(t in Tag, where: t.name == 'pl'))

查看 documentation 了解更多详情。