The exising application uses a database that allows {:array, :binary_id} in a column.
To evaluate an alternative database without too much effort, I want to store the array in a column using embeds_one/3
The original schema had the line
field :previous, {:array, :binary_id}
The new line is
embeds_one :previous, Embed.SessionPrevious, on_replace: :delete
This has worked for other people before and I am probably doing something wrong or have misunderstood the concept. The failing test case is:
defmodule Embed.SessionTest do
use Embed.DataCase
test "previous" do
uuid = Ecto.UUID.generate()
struct =
Embed.Session.changeset(%Embed.Session{}, %{cdr_id: uuid})
|> Ecto.Changeset.apply_action!(:insert)
Embed.Session.changeset(struct, %{cdr_id: Ecto.UUID.generate(), previous: [struct.cdr_id]})
end
end
The code is
defmodule Embed.Session do
@moduledoc "Session struct with CDR id."
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
schema "session" do
field :cdr_id, :binary_id
embeds_one :previous, Embed.SessionPrevious, on_replace: :delete
timestamps()
end
@doc false
def changeset(session, attrs) do
attributes = cast_attributes(attrs)
session
|> Ecto.Changeset.cast(attributes, [:cdr_id])
|> Ecto.Changeset.cast_embed(:previous)
end
defp cast_attributes(%{previous: p} = session) do
embedded = for x <- p, do: %{previous_id: x}
%{session | previous: %{previous_embedded: embedded}}
end
defp cast_attributes(session), do: session
end
defmodule Embed.SessionPrevious do
@moduledoc "Struct with SessionPrevious for Mysql table."
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
embeds_many :previous_embedded, Embed.SessionPreviousId, on_replace: :delete
end
@doc false
def changeset(x, attrs) do
x
|> cast(attrs, attrs)
end
end
defmodule Embed.SessionPreviousId do
@moduledoc "Struct with SessionPreviousId for Mysql table."
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field :previous_id, :binary_id
end
@doc false
def changeset(x, attrs) do
x
|> cast(attrs, attrs)
end
end
Hello, overall I think this code is hardly readable and maintainable:
The schema says embeds_one but the parameter is expected to be an array of ids.
Your private function cast_attributes doesn’t return a Changeset while we are used that cast_* functions return a changeset.
In the test you generate ID twice, I don’t understand, and the test name is not descriptive “previous”.
I understand that this is a demo code however we generally don’t want such hacky code in our codebase. Maybe we could figure out more generally speaking what is the goal?
It would also help to have the error or the unexpected result.
It’s not a good fit for embeds. Embeds allow you to define structured fields. For example maps with fixed keys and types. Or a list of such maps. A list of scalars does not need this.
If you describe your use case and why using array doesn’t work someone will be able to give you better advice. Rather than staying fixed on the current solution.
I am sorry that the code is not readable. That I will try to correct. It is only a demostration of the problem, and as such will not be manined, ever. I promise that nobody will have this sample code in their codebase.
It is not possible to change 1) The goal is to store an array of binary_id, using embeds_one. This particular schema is selected, just as is the database withour arrays storage, because of historical reasons. To use any other way, I have to motivate why. Any help there would be appreciated.
The function cast_attributes/1 can be called
change_the_map_with_attributes_into_another_map_needed_for_cast_embed/1
I am willing to use any function name that helps me finding a solution.
I will change the test to only genereate one UUID, if that helps me finding a solution.
The error:
test previous (Embed.SessionPreviousIdTest)
test/sesson_previous_id_test.exs:4
** (FunctionClauseError) no function clause matching in Ecto.Changeset.cast/6 The following arguments were given to Ecto.Changeset.cast/6: # 1
%Embed.SessionPrevious{id: nil, previous_embedded: } # 2
%{id: :binary_id, previous_embedded: {:embed, %Ecto.Embedded{cardinality: :many, field: :previous_embedded, owner: Embed.SessionPrevious, related: Embed.SessionPreviousId, on_cast: nil, on_replace: :delete, unique: true, ordered: true}}} # 3
%{} # 4
%{previous_embedded: [%{previous_id: “6350438f-0a81-48a5-a7ae-57c4f2b50974”}]} # 5
%{previous_embedded: [%{previous_id: “6350438f-0a81-48a5-a7ae-57c4f2b50974”}]} # 6
Attempted function clauses (showing 3 out of 3): defp cast(%{} = data, %{} = types, %{} = changes, -:invalid-, permitted, _opts) when -is_list(permitted)-
defp cast(%{} = data, %{} = types, %{} = changes, %{} = params, permitted, opts) when -is_list(permitted)-
defp cast(%{}, %{}, %{}, params, permitted, _opts) when -is_list(permitted)- code: Embed.Session.changeset(struct, %{cdr_id: Ecto.UUID.generate(), previous: [struct.cdr_id]})
stacktrace:
(ecto 3.10.3) lib/ecto/changeset.ex:721: Ecto.Changeset.cast/6
(ecto 3.10.3) lib/ecto/changeset.ex:1276: anonymous fn/4 in Ecto.Changeset.on_cast_default/2
(ecto 3.10.3) lib/ecto/changeset/relation.ex:131: Ecto.Changeset.Relation.do_cast/7
(ecto 3.10.3) lib/ecto/changeset/relation.ex:343: Ecto.Changeset.Relation.single_change/5
(ecto 3.10.3) lib/ecto/changeset/relation.ex:112: Ecto.Changeset.Relation.cast/5
(ecto 3.10.3) lib/ecto/changeset.ex:1192: Ecto.Changeset.cast_relation/4
Thank you for the explainaion about embeds.
The reason that an array does not work is that the database does not have arrays.
What is the correct way to store a list of scalars in one column when the databse does not have arrays?
I know about storing each binary_id in its own row, in another table with a session_id column. But only one table is suppose to be used here.
I can switch from embeds_one, if I provide a good motivation.
Then your db probably doesn’t support JSON columns either. In the past we used to convert the array into a string representation and store that. You could then make it into a custom Ecto.Type that packs and unpacks it, so in your code it would just be an array.
Yes, a custom Ecto.Type did sound feasible at the begining. I tried it, but failed (the actual data is more complicated than this example). A college said that he had successfully used JSON, which I thought meant using :map in the dabatase (it is jsonb when I check the table) and embeds_one in the schema. That was my mistake. I should have used Ecto.JSON for the schema instead. That is what I am currently trying.