Ecto schema create assoc without adding to struct

Central to my app is the Account. I have a lot of schemas that belongs_to Account. I like including corresponding has_one/has_many on Account for query/build_assoc conveniences, but I don’t want to clutter the Account struct with an increasing number of non-core associations that will always be #Ecto.Association.NotLoaded<...>. Is there a way to define an association without adding it to the schema struct?

Off the top of my head the features I would most want but so far have worked around would be these:

  • join: m in assoc(a, :my_assoc)
  • Ecto.assoc(account, :my_assoc)
  • Ecto.build_assoc(account, :my_assoc)

I don’t think there’s a way to do that. But your problem could be a symptom of non-optimal design.

Most probably, the problem is the Account schema being central to your app. One solution is to embrace a domain-driven design approach and think of how to separate it in multiple schemas in different bounded contexts. For this you’d have to have deep knowledge of the business domain.

Another approach is to stop relying on Ecto schemas everywhere in your app. They’re a database and/or validation concern. You could use well defined maps coupled with specs. Then you’d care more about the shape of your data and non-core fields won’t bother you that much.

I’m sure you’ll find a satisfactory solution.

You can hide fields you’re not interested in by defining Inspect protocol

@derive {Inspect, optional: [:my_assoc1, :my_assoc2]}

The code above will hide default #Ecto.Association.NotLoaded<...> values for :my_assoc1 and :my_assoc2 fields and will show them only if something else is set.

3 Likes

I appreciated your feedback and ended up going another way, but just to see if what I wanted was technically possible, I made it work in dev by overriding Account.__schema__/2 to support associations that don’t show up in the struct.

  defoverridable __schema__: 2

  def __schema__(:association, :my_assoc) do
    %Ecto.Association.Has{
      cardinality: :one,
      field: :my_assoc,
      owner: __MODULE__,
      related: MyApp.MyContext.MySchema,
      owner_key: :id,
      related_key: :account_id,
      on_cast: nil,
      queryable: MyApp.MyContext.MySchema,
      on_delete: :nothing,
      on_replace: :raise,
      where: [],
      unique: true,
      defaults: [],
      relationship: :child,
      ordered: false,
      preload_order: []
    }
  end

  def __schema__(arg1, arg2) do
    super(arg1, arg2)
  end

I have no plans to use this in production, but it does seem to work with Ecto.assoc(account, :my_assoc), Ecto.build_assoc(account, :my_assoc), and join: m in assoc(a, :my_assoc).