无法在Ecto中创建多对多关联

时间:2016-09-16 06:04:39

标签: elixir phoenix-framework ecto

我正在开发一个多用户,多房间聊天应用程序,我的模型如下所示(为简单起见省略了App模型):

defmodule Elemental.TxChat.User do
  use Elemental.TxChat.Web, :model

  schema "users" do
    # The rooms the user is currenly logged into
    many_to_many :rooms, Elemental.TxChat.Room, join_through: "rooms_users"
    timestamps()
  end

  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [])
    |> validate_required([])
  end
end

defmodule Elemental.TxChat.Room do
  use Elemental.TxChat.Web, :model

  schema "rooms" do
    field :name, :string    
    # The user id that created this room
    field :created_by, :integer
    field :created_from_app, :integer

    many_to_many :members, Elemental.TxChat.User, join_through: "rooms_users"

    timestamps()
  end

  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:name, :created_from_app, :created_by])
    |> validate_required([:name, :created_by, :created_from_app])
  end
end

接下来,我从iex创建了一些房间和用户(每个房间三个)。现在我想知道:假设我希望user1属于room1和room2,而user2属于room2和room3 ......怎么做?

在我看来,虽然定义模式很好,但必须有一个像user1.rooms = [room1, room2]这样的中间步骤。所以我最终在this post上看到了一个build_assoc的例子:

Ecto.build_assoc(current_user, :post)

所以这个应用程序有用户和帖子,并试图链接他们。但我不知道它是如何实现的。数据库/ Ecto如何知道哪个用户ID与哪个帖子ID相关联?

无论如何,我尝试在我的应用iex中执行此操作:

iex(46)> Ecto.build_assoc(user1, :rooms, room1)
%Elemental.TxChat.Room{__meta__: #Ecto.Schema.Metadata<:built, "rooms">,
 created_by: 1, created_from_app: 1, id: 2,
 inserted_at: #Ecto.DateTime<2016-09-16 05:00:00>,
 members: #Ecto.Association.NotLoaded<association :members is not loaded>,
 name: "room1", updated_at: #Ecto.DateTime<2016-09-16 05:00:00>}

我认为函数调用会将user1加入模型Room,使用room1中的数据来查找目标会议室。当我在输出中看到<association :members is not loaded>时,我的心沉了下去,但我想我应该检查数据库。猜猜看,我的加入表中没有条目(rooms_users)。 :(

我认为很明显,模型之间的这些联系需要以某种方式创建,但我似乎已经碰壁了。怎么做?

1 个答案:

答案 0 :(得分:1)

修改
附注:为users_rooms表创建UserRoom模型并创建与UserRoom.changeset / 2的关联会更容易 原始回答

我做了一个小例子项目

defmodule Playground.User do
  use Playground.Web, :model
  alias __MODULE__

  schema "users" do
    field :title, :string
    many_to_many :rooms, Playground.Room, join_through: "users_rooms"

    timestamps()
  end

  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:title])
    |> validate_required([:title])
  end

  def assoc_changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:title])
    |> validate_required([:title])
    |> add_rooms(params, struct)
  end

  defp add_rooms(changeset, params, %User{rooms: rooms}) do
    case params do
      %{add_rooms: to_be_added} when is_list(to_be_added) ->
        changeset |> put_assoc(:rooms, rooms ++ to_be_added)
      _ ->
        changeset
    end
  end
end

工作原理:

iex(1)> u = User.changeset(%User{}, %{title: "u"}) |> Repo.insert!
iex(2)> r1 = Room.changeset(%Room{}, %{title: "r1"}) |> Repo.insert!
iex(3)> r2 = Room.changeset(%Room{}, %{title: "r2”}) |> Repo.insert!

iex(4)> u = User.changeset(u, %{title: "u1"}) |> Repo.update!
%Playground.User{
  rooms: #Ecto.Association.NotLoaded<association :rooms is not loaded>,
  title: "u1",
  ...
}

iex(5)> u = User.assoc_changeset(u, %{add_rooms: [r1]}) |> Repo.update!
error about #Ecto.Association.NotLoaded<association :rooms is not loaded> here

iex(6)> u = Repo.preload(u, :rooms)
%Playground.User{
  rooms: [],
  title: "u1",
  ...
}

iex(7)> u = User.assoc_changeset(u, %{add_rooms: [r1]}) |> Repo.update!
%Playground.User{
  rooms: [
    %Playground.Room{title: "r1", ...}
  ],
  title: "u1",
  ...
}

iex(7)> u = User.assoc_changeset(u, %{add_rooms: [r2]}) |> Repo.update!
%Playground.User{
  rooms: [
    %Playground.Room{title: "r1", ...},
    %Playground.Room{title: "r2", ...}
  ],
  title: "u1",
  ...
}

仍有改进的余地。帮助函数add_rooms / 3可能应该从changeset而不是第三个参数中取出用户房间并更改为add_rooms / 2。
您需要决定还需要改进哪些内容。