如何处理 Phoenix 中的模式多态性?

How to handle schemas polymorphism in Phoenix?

在 Phoenix 中处理多态关联的推荐方法似乎是添加一个包含对其他模式的引用的中间模式:

所以如果我想用不同种类的动物创建模式,我会这样做:

defmodule Animal do
  use Ecto.Model

  schema "animals" do
    belongs_to(:dog, Dog)
    belongs_to(:cat, Cat)
    belongs_to(:owner, PetOwner)
  end
end

defmodule Dog do
  use Ecto.Model

  schema "dogs" do
  end
end

defmodule Cat do
  use Ecto.Model

  schema "cats" do
  end
end

defmodule PetOwner do
  use Ecto.Model

  schema "pet_owners" do
    has_one(:pet, Animal)
  end
end

但我也可以让 PetOwner 模式包含二进制字段和类型:

defmodule Dog do
  use Ecto.Model

  schema "dogs" do
  end
end

defmodule Cat do
  use Ecto.Model

  schema "cats" do
  end
end

defmodule PetOwner do
  use Ecto.Model

  schema "pet_owners" do
    field(:pet, :binary)
    field(:pet_type, :integer)
  end
end

或者甚至只是拥有对所有者模式中所有动物的可为空的引用:

defmodule Dog do
  use Ecto.Model

  schema "dogs" do
    belongs_to(:owner, PetOwner)
  end
end

defmodule Cat do
  use Ecto.Model

  schema "cats" do
    belongs_to(:owner, PetOwner)
  end
end

defmodule PetOwner do
  use Ecto.Model

  schema "pet_owners" do
    has_one(:cat, Cat)
    has_one(:dog, Dog)
  end
end

第一种方法似乎增加了模式的复杂性。不同方法的优缺点是什么?

编辑:假设宠物主人只能拥有一只宠物,如果模式允许多只宠物,则验证在变更集中完成。

我花了很多时间阅读博客 posts 和类似问题的答案。我也在 E​​lixir 的讨论中问了这个问题:https://elixirforum.com/t/how-to-handle-schemas-polymorphism-in-phoenix/13269/24 并得到了很好的回答。

一个反复出现的问题是,这个问题更像是一个 SQL 问题,而不是 Phoenix 或 Ecto 问题。 确实Ecto提供了一些方法来解决这个问题,但是这个问题的解决应该从:"How to handle polymorphic association in a relational database".

开始

注意,如果你在这里寻找解决"belong_to"多态关联的方案(即如果一个具体的table属于两个或多个多态table),则有是 Ecto 文档中的 entire section。此答案适用于 "has_many" 多态关联。

这是 ndac_todoroki 在 Elixir 论坛上对我的 post 的回答所写的不同方法的比较,所有功劳都归于他:

单一Table继承

这是当你有一个大 table(动物)并且每个具体动物 table 都是那个 table 的 sub-division 时。您不会有多个 table。

优点

  • 你将只有一个table。
  • 抓取所有动物非常容易
  • 它可以很容易地在 Ecto 中实现,因为所有具体的动物模块都可以具有彼此不同但引用相同大的模式 table。

缺点

  • 如果你想要每种动物的唯一字段,你到处都会得到 NULL 列(但在使用具体模块的模式时你不关心:它不会出现。)

Class Table 继承

这是当你有一个基 table(= 动物)时,每个具体的动物 table 都有一个对基 table 及其独特字段的引用。 (动物{id:1,出生:“20180101”,接种疫苗:true}动物{id:2,出生:“20111225”,接种疫苗:false /猫{animal_id:1,颜色:"brown" } 蛇 {animal_id: 2, 长度: 150})

优点

  • 列出所有动物很容易,如果您只需要基本信息
  • 没有 NULL 列

缺点

  • 虽然从具体模块获取(动物的)完整信息非常容易(您只需加入 :animal),但从基础端获取完整信息却不是(您不知道要加入什么)。
  • 这是除非你在Rails(animal_type和animal_id)中写一些类似于多态table的逻辑,这被称为“不可取”在 Ecto 文档中。

具体Table继承

不会创建 table 动物。每个具体动物 tables 将具有所有基本信息 + 它们的独特信息。如果您想确保所有动物都是动物,但您不直接使用 Animals,这会很好。 (我宁愿创建一个名为 Animal 的协议也不愿这样做)

优点

  • 获取具体动物时无需加入
  • 与 Ecto 的抽象 tables 配合得很好(动物将是一个抽象 table)

缺点

  • 你需要小心迁移不要破坏整个继承
  • 独特的断言必须完全在应用程序逻辑上完成
  • 尤其是 ID 在所有动物 table 中必须是唯一的,因此最好使用 :binary_ids。
  • 在 Animal 中使用键的任何搜索都应该遍历所有 tables(呃)

摘要tables

这在 Ecto 文档中有描述。 (示例回购)仅使用它似乎对您的使用没有太大帮助,但如果您正在查看 STI 或 CTI,它可能有助于实现。

当您创建 Class Table 继承时,每个 table 指的是一个 Animal 基础 table。使用抽象 tables,您可以将每个具体动物 tables 分成多个基础 tables,例如猫 table 将引用 tocat_base 和蛇形到 snake_base,其中 cat_base 和 snake_base 将具有相同的列。然后我们将创建一个抽象的 table 动物,当你 cat |> Animal.add_base_animal_info() 它将创建一个 cat_base.

优点

  • 您可以从 Animals 中检索具体的动物

缺点

  • 你需要加入每个具体的 *_base tables 来做 list_animals

我认为这介于 Class Table 继承和具体 Table 继承之间。

嵌入数据

嵌入可以在 Postgres 和 MongoDB 等中完成。您可以让一个动物 table 带有一个接受地图的字段(字段:详细信息,:地图)。然后定义许多具体的动物模块,其模式指的是该动物 table,具有 embeds_one :details,CatDetails,其中您为 CatDetails 定义 embed_schema。 (此示例是带有嵌入的 STI)

优点

  • tables会很干净
  • 没有 NULL
  • 列出所有动物很容易
  • 使用具体动物的模式存储数据将验证您传递的地图的形状

缺点

  • 里面地图的形状:细节无法在数据库级别验证
  • 在嵌入中搜索字段可能不太好(取决于您使用的数据库)