Ecto.Schema and Access behaviour

Hey everyone, coming from Rails and new to Elixir, so bear with me as I’m still learning.

I’m currently writing tests and as I want them to run fast, I’m going to skip the database if possible. So instead of updating associations and reloading them to have the data available in the tests, I want to do something like this

user |> put_in(["integrations", "state"], "deleted") # this marks the integration as deleted

but this blows up with: User does not implement the Access behaviour Both User and Integration are Ecto schemas.

Am I doing something wrong? Why isn’t the Access behaviour implemented for Ecto.Schema's?

2 Likes

You cannot use put_in like that as structs (which the schemas are) don’t support Access behaviour. So you need to use Access.key! for example:

user |> put_in([Access.key!(:integrations), Access.key!(:state)], "deleted")

or you can nest the map update syntax:

%{put_in | integrations: %{put_in.integrations | state: "deleted"}}

I assumed in both your keys are actually atoms like they are usually in schemas.

4 Likes

If you really need it, maybe you can implement this behaviour yourself by using either Map.from_struct or some metaprogramming (it’s just 4 callbacks).

defmodule User do
  @behaviour Access

  # ... schema and stuff

  # Access callbacks the easy way
  def fetch(term, key) do
    term
    |> Map.from_struct()
    |> Map.fetch(key)
  end

  def get(term, key, default) do
    term
    |> Map.from_struct()
    |> Map.get(key, default)
  end

  def get_and_update(data, key, function) do
    data
    |> Map.from_struct()
    |> Map.get_and_update(key, function)
  end

  def pop(data, key) do
    data
    |> Map.from_struct()
    |> Map.pop(key)
  end
end

You might also want to read this

But in ecto the schema structs usually represent rows in a table, and modifications to these rows are then done via changesets. So your example can be probably expressed via them.

https://hexdocs.pm/ecto/Ecto.Changeset.html#module-associations-embeds-and-on-replace

^^^ see maybe_mark_for_deletion/1

2 Likes

Ah if only @derive [Access] still worked. Protocol’izing Access was inefficient in the old days, however if rewritten in my ProtocolEx then it would have no runtime cost at all, even faster than the built-in one as it stands. :wink:

The problem is that Access is used heavily also when protocols are not yet consolidated - one big example is during compilation.

1 Like

Well for compiling the access implementations itself they should either be defined in things that don’t use access or do direct calls to the proper implementations themselves, after that it should matter for any user code regardless. :slight_smile:

Hmm, potential ideas for reifying that into the API instead of calling it as necessary manually…
Do note, mine supports staged recompilation, so you can consolidate it when a base set that could then be used in other implementations later and recompiled as necessary if something depends on a specific implementation. :slight_smile:

Ah if only Elixir were typed, then you wouldn’t need such hacks. ^.^