halostatue
Clearing an `embeds_many` entry (`cast_embed/3`)
If I am using cast_embed/3, how do I clear an embeds_many entry?
I have a table flows that has a options JSONB column
CREATE TABLE flows (
id bigint generated always as identity primary key,
options JSONB
);
I have defined the schema as follows:
defmodule Test.Flow do
use Ecto.Schema
import Ecto.Changeset
defmodule Option do
use Ecto.Schema
import Ecto.Changeset
@primary_key false
embedded_schema do
field :value, :string
end
def changeset(opts \\ %__MODULE__{}, attrs) do
cast(opts, attrs, [:value])
end
end
schema "flows" do
embeds_many :options, Option, on_replace: :delete
end
def changeset(flows \\ %__MODULE__{}, attrs) do
flows
|> cast(attrs, [:id])
|> cast_embed(:options)
|> validate_length(:options, min: 1)
end
end
In Typescript type terms, the type would look something like this:
interface Flow {
id: number
options?: [Option, ...Option[]] | null
}
interface Option {
value: string
}
That is, options should be null or an array of at least one Option. But I can’t make that happen:
iex(1)> {:ok, f} = Repo.insert(Test.Flow.changeset(%{})
{:ok,
%Test.Flow{
__meta__: #Ecto.Schema.Metadata<:loaded, "flows">,
id: 1,
options: []
}}
iex(2)> Test.Flow.changeset(f, %{options: nil})
#Ecto.Changeset<
action: nil,
changes: %{},
errors: [options: {"is invalid", [validation: :embed, type: {:array, :map}]}],
data: #Test.Flow<>,
valid?: false
>
iex(3)> Repo.update!(f, %{options: [%{}]})
%Test.Flow{
__meta__: #Ecto.Schema.Metadata<:loaded, "flows">,
id: 1,
options: [%Test.Flow.Option{value: nil}]
}
iex(4)> f = Repo.get(TestFlow, 1)
%Test.Flow{
__meta__: #Ecto.Schema.Metadata<:loaded, "flows">,
id: 1,
options: [%Test.Flow.Option{value: nil}]
}
iex(5)> Test.Flow.changeset(f, %{options: []})
#Ecto.Changeset<
action: nil,
changes: %{
options: [
#Ecto.Changeset<action: :replace, changes: %{}, errors: [],
data: #Test.Flow.Option<>, valid?: true>
]
},
errors: [
options: {"should have at least %{count} item(s)",
[count: 1, validation: :length, kind: :min, type: :list]}
],
data: #Test.Flow<>,
valid?: false
>
This feels like it should be possible, if not easy, but I don’t really see a way to do it, especially since there doesn’t appear to be a distinction between attrs of %{} (options is missing) and %{options: nil} (options is explicitly nulled). That is, when I do Test.Flow.changeset(f, %{options: nil}).changes, I get %{}.
I suppose that I could use a sigil value (:none), but that feels…awkward.
Marked As Solved
halostatue
One workaround is to define an Ecto.Type. My example here isn’t well organized (the type here should be a different module, OptionList), but that’s fine for an example:
defmodule Test.Flow do
use Ecto.Schema
import Ecto.Changeset
defmodule Option do
use Ecto.Schema
use Ecto.Type
import Ecto.Changeset
@primary_key false
embedded_schema do
field :value, :string
end
def changeset(opts \\ %__MODULE__{}, attrs) do
cast(opts, attrs, [:value])
end
@impl Ecto.Type
def type, do: {:array, :map}
@impl Ecto.Type
def cast([_ | _] = list) do
cond do
Enum.all?(list, &is_struct(&1, __MODULE__)) -> {:ok, list}
Enum.all?(list, &is_map/1) -> load(list)
true -> :error
end
end
def cast(nil), do: {:ok, nil}
def cast(_), do: :error
@impl Ecto.Type
def load(nil), do: {:ok, nil}
def load(data) when is_list(data) do
{
:ok,
Enum.map(data, fn entry ->
struct!(
__MODULE__,
for {k, v} <- entry do
{String.to_existing_atom(k), v}
end
)
end)
}
end
@impl Ecto.Type
def dump([_ | _] = list) do
if Enum.all?(list, &match?(%__MODULE__{}, &1)) do
{:ok, Enum.map(list, &Map.from_struct/1)}
else
:error
end
end
def dump(nil), do: {:ok, nil}
def dump(_), do: :error
end
schema "flows" do
field :options, Option
end
def changeset(flows \\ %__MODULE__{}, attrs) do
cast(flows, attrs, [:id, :options])
end
end
It’s a lot of work, and I suspect that some of it could be wrapped in a macro.
I fear that the lesson here is to avoid embeds_many if your column is intentionally nullable, because Ecto works against your design in this case.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








