无法在 Ecto 中创建多对多关联
Unable to create many to many associations in Ecto
我正在开发一个多用户、多房间的聊天应用程序,我的模型如下(为简单起见省略了应用程序模型):
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)
所以这个应用程序有用户和 posts,并且正在尝试 link 他们。但我看不出它是如何实现的。 database/Ecto 如何知道哪个用户 ID link 与哪个 post 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>
时,我的心沉了下去,但我认为我应该检查数据库。你猜怎么着,我加入的 table (rooms_users) 中没有条目。 :(
我认为很明显模型之间的这些 link 需要以某种方式创建,但我似乎碰壁了。如何做到这一点?
编辑
旁注:为 users_rooms table 创建 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 可能应该从变更集中获取用户房间而不是第三个参数,并更改为 add_rooms/2.
其他需要改进的地方由您决定。
我正在开发一个多用户、多房间的聊天应用程序,我的模型如下(为简单起见省略了应用程序模型):
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)
所以这个应用程序有用户和 posts,并且正在尝试 link 他们。但我看不出它是如何实现的。 database/Ecto 如何知道哪个用户 ID link 与哪个 post 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>
时,我的心沉了下去,但我认为我应该检查数据库。你猜怎么着,我加入的 table (rooms_users) 中没有条目。 :(
我认为很明显模型之间的这些 link 需要以某种方式创建,但我似乎碰壁了。如何做到这一点?
编辑
旁注:为 users_rooms table 创建 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 可能应该从变更集中获取用户房间而不是第三个参数,并更改为 add_rooms/2.
其他需要改进的地方由您决定。