Embedded schemas vs plain old structs

Hi all,

I recently looked into what can and cannot be done with embedded schemas. I realize that they are basically structs on steroids since you get all the benefits of structs + validation and casting for free. The ability to type all keys also documents better the code.

So, why should I bother using the plain old structs instead of embedded schemas everywhere?

1 Like

You can think of embedded schemas just as convenience to declare the data+types together. But you don’t need them. For example, you can also do this:

defmodule Post do
  defstruct :title
end

data = %Post{}
types = %{title: :string}
Ecto.Changeset.cast({data, types}, params, [:title])

and it would work the same.

10 Likes

This is a very powerful thing indeed! These days I was implementing a “custom fields” feature for a table on a side project I had, and with this I could instantiate changesets to validate the data the user inserted to these custom fields, without having to create an embedded schema dynamically for that (not even sure if that’s possible).

Here is the code: blessd_umbrella/apps/blessd/lib/blessd/shared/custom_data.ex at master · 0ks/blessd_umbrella · GitHub

PS.: It’s quite complex, I’m sure some refactors would fit there, but just as an example of how that is useful.

4 Likes

This is also why Ecto is useful for generic validation as well, not just SQL stuff. :slight_smile:

4 Likes

True! I got so happy when they separated ecto and ecto_sql. Now I just add ecto to some apps inside my umbrella that need validation but have no database.

3 Likes

Yep schemaless changesets right?

While it’s equivalent it’s also less convenient (your words not mine). So why should I bother defining plain old structs instead of using embedded schemas every time?

In short: because it’s simpler to implement and to use plain old structs than embedded schemas.

I use plain old structs when I don’t need any validation or type casting, for example. Once validations are needed I turn them into embedded schemas.

I get you if you think that it would be good to at least have type casting for everything. But IMO the complexity of adding it does not pay off every single time.

4 Likes

For comparison, this:

defmodule Person do
  defstruct [:name, :age]
end

struct = %Person{name: "Kelvin", age: 26}

Would be something like this:

defmodule Person do
  use Ecto.Schema
  import Ecto.Changeset

  embedded_schema do
    field :name, :string
    field :age, :integer
  end

  def new(attrs) do
    %Person{}
    |> cast(attrs, [:name, :age])
    |> apply_changes()
  end
end

struct = Person.new(%{name: "Kelvin", age: 26})

Of course it’s not fair to compare because the second one actually would handle a lot more of cases, like maps with string keys, and even would properly cast "26" to 26 on the age attribute. But if the one that is giving you these data is yourself (or another programmer), why bother about casting or string keys?

And even if you needed to guarantee the person would have the correct type, I would then first do something like:

defmodule Person do
  defstruct [:name, :age]

  def new(%{name: name, age: age}) when is_bitstring(name) and is_integer(age) do
    %Person{name: name, age: age}
  end  
end

This way, if the programmer make the mistake, he/she will get a FunctionClauseError, instead of just ignoring his mistake of giving a string for the person age.

3 Likes