I can’t work out how Ecto works with autogenerating a new primary key when inserting a changed changeset. I run afoul of its not-null
and unique
constraints and I’ve probably just stared myself blind at this point. Hoping y’all have some opinions and knowledge I can lean into.
Given a simple schema like this:
defmodule Switch do
use Ecto.Schema
import Ecto.Changeset
schema "switches" do
field :switch_id, :string
field :state, :boolean, default: false
timestamps(updated_at: false, type: :utc_datetime_usec)
end
def changeset(schema \\ %__MODULE__{}, params) do
schema
|> cast(params, [:switch_id, :state])
|> validate_required([:switch_id])
end
end
I now want a context module that lets me:
- Get
Switch
changesets (so a page can put them into forms). - Insert a changed changeset such that it becomes a new row (thereby “soft-deleting” the previous row).
Below is how I want the context module to work, but since it doesn’t work it means I’m misunderstanding something about how Ecto treats nil values and autogenerating keys.
defmodule Switches do
import Ecto.Changeset
import Ecto.Query
alias Switch
# Data Retrieval
def get_switch(switch_id) do
from(switch in Switch)
|> where([switch], switch.switch_id == ^switch_id)
|> order_by(desc: :id)
|> limit(1)
|> Repo.one()
end
# Validation & Insertion
def prepare_switch(switch_id) do
case get_switch(switch_id) do
nil -> Switch.changeset(%{switch_id: switch_id})
switch -> Switch.changeset(switch, %{})
end
end
def insert_switch(changeset) do
Repo.insert(changeset)
end
def change_switch(data \\ %Switch{}, params) do
changeset = Switch.changeset(data, params)
if changeset.changes == %{} do
changeset
else
changeset |> put_change(:id, nil)
end
end
end
With this, a new switch can be inserted:
iex> prepare_switch("foo")
|> insert_switch()
%Switch{
__meta__: #Ecto.Schema.Metadata<:loaded, "switches">,
id: 1,
switch_id: "foo",
state: false,
inserted_at: ~U[2024-07-08 16:24:59.157137Z]
}}
But I run afoul of constraint errors when inserting a change:
iex> prepare_switch("foo")
|> change_switch(%{state: true})
|> insert_switch()
** (Postgrex.Error) ERROR 23502 (not_null_violation) null value in column "id" of relation "switches" violates not-null constraint
table: switches
column: id
Failing row contains (null, foo, f, 2024-07-08 16:24:59.157137).
Now, I thought setting id
to nil in the change_switch
function would cause Ecto to generate a new ID, but it seems to take the nil as a literal null-insertion. Whoops, my bad.
But then how are we supposed to create new entries from an existing changeset?
The migration, if that's relevant
def change do
create table(:switches) do
add :switch_id, :string, null: false
add :state, :boolean, default: false, null: false
timestamps(updated_at: false, type: :utc_datetime_usec)
end
end