Ecto belongs_to changeset insert 在两个关系中插入
Ecto belongs_to changeset insert inserts in both relations
简介
我对 Ecto 的关系有疑问。我有两个模式 User 和 Role,我希望能够插入一个新的 User数据库中的外键 Role table.
问题是每次我尝试插入新用户时,它都与 角色 table (role_id 列在 User table 中为空。我试图建立一个 assoc 来创建关系,但它不起作用。
当前代码
我的用户架构是:
schema "user" do
field :first_name, :string
field :last_name, :string
field :email, :string
field :encrypted_password, :string
field :password, :string, virtual: true
belongs_to :role, TestApp.Role
timestamps()
end
角色 架构:
schema "role" do
field :name, :string
timestamps()
has_many :users, TestApp.User
end
我将 User 变更集定义如下:
@required_fields ~w(first_name last_name email password)
@optional_fields ~w(encrypted_password)
@required_update_fields ~w()
@optional_update_fields ~w(first_name last_name email password encrypted_password role)
def create_changeset(struct, params \ :empty) do
changeset(struct, params, @required_fields, @optional_fields)
end
def update_changeset(struct, params \ :empty) do
changeset(struct, params, @required_update_fields, @optional_update_fields)
end
defp changeset(struct, params \ :empty, required_field, optional_fiels) do
struct
|> cast(params, required_field, optional_fiels)
|> validate_format(:email, ~r/@/, message: "invalid format")
|> validate_length(:password, min: 5)
|> validate_confirmation(:password, message: "password does not match")
|> unique_constraint(:email, message: "email already taken")
|> generate_encrypted_password
end
和角色变更集:
def changeset(struct, params \ %{}) do
struct
|> cast(params, [:name])
|> validate_required([:name])
|> unique_constraint(:name)
end
在我的 UserController 中,我有一个 create 方法:
def create(conn, %{"user" => user_params}) do
changeset = User.create_changeset(%User{}, user_params)
case User.insert(changeset) do
{:ok, user} ->
conn
|> put_status(:ok)
|> render(TestApp.UserRender, "show.json", user: user)
{:error, changeset} ->
handle_user_creation_validation_error(conn, changeset: changeset)
end
end
经过测试的解决方案(未成功)
我测试了几个选项,下面的列表只是我到目前为止尝试过的一个例子(尝试太多)。它根本不需要编译,我只是post它作为信息。
我试图在 User 中创建一个 insert 方法来构建关联并保存变更集。
def insert(changeset) do
if changeset.valid? do
IEx.pry
TestApp.Role.find_by_name("user")
|> Ecto.build_assoc(:users)
|> Ecto.change(changeset.params)
|> Repo.insert
else
{:error, changeset}
end
end
我也尝试过获得角色并建立一个 assoc
TestApp.Role.find_by_name("user")
|> TestApp.Repo.preload(:users)
|> Ecto.build_assoc(:users)
|> Ecto.Changeset.put_assoc(:users, user)
|> Repo.insert!
我在 Ecto 文档中看到了这个例子
def insert(changeset) do
Repo.transaction fn ->
TestApp.Role.find_by_name("user")
|> TestApp.Repo.preload(:users)
|> Ecto.Changeset.put_assoc(:users, [changeset])
|> Repo.insert
end
现状
我能够在 users 和 roles 之间创建关系,但每次我插入一个新的 user 还插入了一个新的 role 角色(我很震惊,它每次都创建一个 one_to_one 关系!)。
我知道有一个正确的方法,但我想知道它是否意味着首先从 角色 预加载 :users
。我不认为这是一个 acceptable 解决方案,希望有人能解释所有关于关系、预加载和保存变更集的内容。
解决方案
好吧,似乎有必要将 FK 作为参数发送到 User 变更集中,所以我不得不将参数字段 role
更改为 role_id
.
参数正文请求示例可能是:
{
"user" : {
"first_name": "first name",
"last_name": "last name",
"email": "test@email",
"password": "longtestpassword",
"password_confirmation": "longtestpassword",
"role_id": 1
}
}
我还向变更集验证管道添加了一个 cast_assoc
:
|> cast_assoc(:role)
删除了 User.insert/1
实现,控制器调用 Repo.insert/1
。
感谢您的帮助!
改变
@optional_update_fields ~w(... role)
至:
@optional_update_fields ~w(... role_id)
然后只在您的参数中发送 role_id 而不是角色字段映射
通常我用这些方法来处理这种情况:
来自user
方:
- 直接把
role_id
(这是一定的Role.id
)放在User changese attrs
中,这样在Repo.insert(用户)之后,这个新用户就会有ForeignKey
的 Role
,像这样:
def changeset(%User{}=user, %{.., role_id: certain_role_id}=attrs) do
user
|> cast(attrs, [...,:role_id]
|> validate_require([:role_id, ...])
- build_assoc,实际上和上一个一样,只需按 "role_id"
进入
user
,但更美丽..
来自role
方:
- put_assoc,在
Role changeset
中使用,它将管理put_assoc(:users, [user_changeset_lists])
与当前role
关联的所有users
。由于put_assoc
不检查数据,所以最好做 changeset
manual;,like this:
def changeset(%Role{}=role, new_user) do
users = Repo.preload(role, :users)
users = user ++ User.changeset(new_user)
role
|> cast(..)
|> validate_required(..)
|> put_assoc(:users, users)
end
- cast_assoc like add
user
for role
without affects alreasy related user
, and cast_assoc
will auto invoke User.changeset
, 像这样:
def changeset(%Role{}=role, %{users: [new_user1, new_user2]}) do
role
...
|> cast_assoc(:users)
end
简介
我对 Ecto 的关系有疑问。我有两个模式 User 和 Role,我希望能够插入一个新的 User数据库中的外键 Role table.
问题是每次我尝试插入新用户时,它都与 角色 table (role_id 列在 User table 中为空。我试图建立一个 assoc 来创建关系,但它不起作用。
当前代码
我的用户架构是:
schema "user" do
field :first_name, :string
field :last_name, :string
field :email, :string
field :encrypted_password, :string
field :password, :string, virtual: true
belongs_to :role, TestApp.Role
timestamps()
end
角色 架构:
schema "role" do
field :name, :string
timestamps()
has_many :users, TestApp.User
end
我将 User 变更集定义如下:
@required_fields ~w(first_name last_name email password)
@optional_fields ~w(encrypted_password)
@required_update_fields ~w()
@optional_update_fields ~w(first_name last_name email password encrypted_password role)
def create_changeset(struct, params \ :empty) do
changeset(struct, params, @required_fields, @optional_fields)
end
def update_changeset(struct, params \ :empty) do
changeset(struct, params, @required_update_fields, @optional_update_fields)
end
defp changeset(struct, params \ :empty, required_field, optional_fiels) do
struct
|> cast(params, required_field, optional_fiels)
|> validate_format(:email, ~r/@/, message: "invalid format")
|> validate_length(:password, min: 5)
|> validate_confirmation(:password, message: "password does not match")
|> unique_constraint(:email, message: "email already taken")
|> generate_encrypted_password
end
和角色变更集:
def changeset(struct, params \ %{}) do
struct
|> cast(params, [:name])
|> validate_required([:name])
|> unique_constraint(:name)
end
在我的 UserController 中,我有一个 create 方法:
def create(conn, %{"user" => user_params}) do
changeset = User.create_changeset(%User{}, user_params)
case User.insert(changeset) do
{:ok, user} ->
conn
|> put_status(:ok)
|> render(TestApp.UserRender, "show.json", user: user)
{:error, changeset} ->
handle_user_creation_validation_error(conn, changeset: changeset)
end
end
经过测试的解决方案(未成功)
我测试了几个选项,下面的列表只是我到目前为止尝试过的一个例子(尝试太多)。它根本不需要编译,我只是post它作为信息。
我试图在 User 中创建一个 insert 方法来构建关联并保存变更集。
def insert(changeset) do
if changeset.valid? do
IEx.pry
TestApp.Role.find_by_name("user")
|> Ecto.build_assoc(:users)
|> Ecto.change(changeset.params)
|> Repo.insert
else
{:error, changeset}
end
end
我也尝试过获得角色并建立一个 assoc
TestApp.Role.find_by_name("user")
|> TestApp.Repo.preload(:users)
|> Ecto.build_assoc(:users)
|> Ecto.Changeset.put_assoc(:users, user)
|> Repo.insert!
我在 Ecto 文档中看到了这个例子
def insert(changeset) do
Repo.transaction fn ->
TestApp.Role.find_by_name("user")
|> TestApp.Repo.preload(:users)
|> Ecto.Changeset.put_assoc(:users, [changeset])
|> Repo.insert
end
现状
我能够在 users 和 roles 之间创建关系,但每次我插入一个新的 user 还插入了一个新的 role 角色(我很震惊,它每次都创建一个 one_to_one 关系!)。
我知道有一个正确的方法,但我想知道它是否意味着首先从 角色 预加载 :users
。我不认为这是一个 acceptable 解决方案,希望有人能解释所有关于关系、预加载和保存变更集的内容。
解决方案
好吧,似乎有必要将 FK 作为参数发送到 User 变更集中,所以我不得不将参数字段 role
更改为 role_id
.
参数正文请求示例可能是:
{
"user" : {
"first_name": "first name",
"last_name": "last name",
"email": "test@email",
"password": "longtestpassword",
"password_confirmation": "longtestpassword",
"role_id": 1
}
}
我还向变更集验证管道添加了一个 cast_assoc
:
|> cast_assoc(:role)
删除了 User.insert/1
实现,控制器调用 Repo.insert/1
。
感谢您的帮助!
改变
@optional_update_fields ~w(... role)
至:
@optional_update_fields ~w(... role_id)
然后只在您的参数中发送 role_id 而不是角色字段映射
通常我用这些方法来处理这种情况:
来自user
方:
- 直接把
role_id
(这是一定的Role.id
)放在User changese attrs
中,这样在Repo.insert(用户)之后,这个新用户就会有ForeignKey
的Role
,像这样:
def changeset(%User{}=user, %{.., role_id: certain_role_id}=attrs) do user |> cast(attrs, [...,:role_id] |> validate_require([:role_id, ...])
- build_assoc,实际上和上一个一样,只需按 "role_id"
进入
user
,但更美丽..
来自role
方:
- put_assoc,在
Role changeset
中使用,它将管理put_assoc(:users, [user_changeset_lists])
与当前role
关联的所有users
。由于put_assoc
不检查数据,所以最好做changeset
manual;,like this:
def changeset(%Role{}=role, new_user) do users = Repo.preload(role, :users) users = user ++ User.changeset(new_user) role |> cast(..) |> validate_required(..) |> put_assoc(:users, users) end
- cast_assoc like add
user
forrole
without affects alreasy relateduser
, andcast_assoc
will auto invokeUser.changeset
, 像这样:
def changeset(%Role{}=role, %{users: [new_user1, new_user2]}) do role ... |> cast_assoc(:users) end