This is only checked when the module in question is compiled. To me it mirrors the implementation of behaviors, and is best checked by the tool itself (leveraging the compiler). A unit test only checks a specific application of a pattern. Self verifying patterns are a powerful way to make invalid states unrepresentable.
It would be mega cheap to check, so I wouldn’t personally worry about performance.
This discussion reminds me of how I thought embeds work in Ecto, when I was first learning about them: intuitively I thought the fields of an embed would be “splat” out into the parent table. I probably got this “intuition” from other ORM’s I’ve worked with in the past in Java, where this is an option (too long ago that I actually remember how that worked).
I quickly adjusted my thinking after learning and reading about embeds. But I think it’s an interesting misunderstanding I had at first.
It also touches on the topic of “Value Objects” in the jargon of Domain Driven Design (DDD). This school of thought pushes heavily in the direction of small immutable objects, encapsulating data in a simple and reusable way (as opposed to “entities” that have a long-lived lifecycle). I’m not mentioning it because that’s where I want to go, but because I think the idea of small data structures (structs) should be encouraged, but Ecto doesn’t always makes that easy. Embeds can be used for this purpose (and I already do use them heavily, and it serves me greatly). But it still causes some friction, because now you have all that data in one column, and you have to be knowledgeable about jsonb, how to index properly and how to query with these nested fields.
Thought experiment: what if there were two different embedding “styles”? One where all the data resides in one column. And another style where the data is splatted into the parent table, using multiple columns.
PS: sorry if this is going off-topic here. I hope this is still in line with brainstorming about ways to accomplish reuse in Ecto schemas. But I realise there is nothing here about inheritance.
Yes while sharing a bunch of fields between two modules is nothing complicated, my original idea was to make schemas extensible without modification.
What if you want to extend Oban.Job for instance, or a shared schema module between apps that share a database.
Of course those use cases carry a lot of smells with them but that’s what you get with 10years old codebases where 20 different devs of different levels were involved.
I’ll decide with my team whether we self-join or extend, so personnally I’m okay if you derive the topic But originally it was indeed about inheritance or at least about sharing a database table.
Y’know, I’ve just had a thought. It is simpler even than I originally thought . This is probably the way to do it, and “just works” AFAIK.
defmodule Vehicle do
use Ecto.Schema
schema "schema" do
field :capacity, :integer
end
end
defmodule Boat do
use Ecto.Schema
@primary_key false
schema "schema" do
for field <- Vehicle.__schema__(:fields) do
field field, Vehicle.__schema__(:type, field)
end
field :something_extra, :type
end
end
It is still not something I would do in this way, I would prefer the slightest indirection (sharing something that two schemas compose) as the signal that it is, but in terms of how you could do what you asked, this would be by far the simplest way.
For what it’s worth, I made a library (Flint) to help do this (among many other things with Ecto embedded_schemas)
You can achieve what you want, including inheriting config attributes, using Flint.Extensions.
Example:
defmodule Inherited do
use Flint.Extension
attribute :schema_prefix
attribute :schema_context
attribute :primary_key, default: false
attribute :timestamp_opts, default: [type: :naive_datetime]
embedded_schema do
field! :timestamp, :utc_datetime_usec
field! :id
embeds_one :child, Child do
field :name, :string
field :age, :integer
end
end
end
And then you can use it like:
defmodule Schema do
use Flint.Schema, extensions: [Inherited]
embedded_schema do
field :type, :string
end
end
Then you will have those inherited contents of the embedded_schema defined in the extension:
I started with the indirection approach, defining all base fields in a separate macro. It worked well but we needed to check if the embedding is null or not in the base model to know if we could do some operations. So I added this migration:
add :is_embedded,
:boolean,
generated:
"ALWAYS AS (some_extension_field IS NOT NULL AND some_extension_field > 0.0) STORED"
And this field:
field :is_embedded, :boolean, read_after_writes: true, writable: :never
But Ecto gives me this error when trying to insert:
** (Postgrex.Error) ERROR 42703 (undefined_column) column "is_embedded" does not exist
query: INSERT INTO "my_table" ("a","b","c","id") VALUES ($1,$2,$3,$4) RETURNING "is_embedded"
I’m going to move that float field used in the generated column back to the base schema (although it does not belong here) and try to use a regular column.
Besides, I just found the :load_in_query option for Ecto schema fields. I think that it could be a simple solution but we will need to add the field back in a lot of places.
Edit: just using :load_in_query seems good enough for now. But it’s error prone. I think I’m going to try the “relation to self” approach, this will give a more explicit “not loaded association” error.