I’m not sure if this can or should be handled automatically by Repo.insert (probably not), but I’m trying to figure out a way to implicitly replace a singleton entity associated with another instead of receiving a uniqueness violation error.
I have a simple User model with an associated Avatar via a has_one and belong_to relationship.
I am using phoenix_ecto ~> 4.0, ecto_sql ~> 3.0 and posgrex ~> 0.0.0. I’m also using arc_ecto to make my file uploads to S3 easier, but that’s not too relevant (I just included it in the following snippets to be thorough).
Here is the User model:
defmodule Example.User do
use Example.Web, :model
schema "users" do
field :email, :string
field :username, :string
# ...
has_one :avatar, Example.Avatar
end
# ...
end
The Avatar model:
defmodule Example.Avatar do
use Example.Web, :model
use Arc.Ecto.Schema
alias Example.{Avatar, User, Media}
schema "avatars" do
field :file, Media.Avatar.Type # `arc_ecto` construct
belongs_to :user, User, on_replace: :update
timestamps()
end
def changeset(%Avatar{} = struct, params \\ %{}) do
struct
|> cast(params, [:file])
|> cast_attachments(params, [:file])
|> validate_required([:file])
|> put_assoc(:user, params.user)
|> unique_constraint(:user, name: :avatars_users_id_index)
end
end
The migration for creating the Avatar entity:
defmodule Example.Repo.Migrations.CreateAvatar do
use Ecto.Migration
def change do
create table(:avatars) do
add :file, :string # @see https://github.com/stavro/arc_ecto#add-a-string-column-to-your-schema
add :user_id, references(:users, on_replace: :update)
timestamps()
end
create unique_index(:avatars, [:user_id])
end
end
The controller for Avatar:
defmodule Example.AvatarController do
use Example.Web, :controller
alias Example.{Avatar, Auth}
def create(conn, %{file: file}) do
user = Auth.Guardian.Plug.current_resource(conn) # fetches session user
changeset = Avatar.changeset(%Avatar{}, %{file: file, user: user})
case Repo.insert(changeset) do
{:ok, avatar} ->
conn
|> put_status(:created)
|> put_resp_header("location", avatar_path(conn, :show, avatar))
|> render("show.json", avatar: avatar)
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> put_view(Example.ChangesetView)
|> render("error.json", changeset: changeset)
end
end
end
Unsurprisingly, first Avatar that I create for a User via a POST request works just fine.
The “problem” is that a subsequent POST ends up resulting in a unique constraint violation:
{"errors":{"user":["has already been taken"]}}
I was expecting the single Avatar entity to be implicitly updated/replaced, but I’m thinking that’s beyond the scope and semantics of Repo.insert (and, arguably, POST).
Should I instead just conditionally perform a PATCH/PUT (and thus a conditional Repo.update) from my client depending on whether the Avatar entity already exists for the User? This works just fine, but I’d love to avoid conditional request logic in the client if possible.
I think the answer is pretty obvious as I’ve reached the end of this question - just perform conditional POST and PUT/PATCH requests and call Repo.insert and Repo.update accordingly. In fact, I think it must be done this way. Otherwise Ecto would never be able to report a constraint violation for 1:1 relationships upon insertion, and that is truly problematic.
Anyways, I thought it was worth posting just in case someone else thinks about things in a similar way and has the same question.
Thanks!




















