jmaniex

jmaniex

Managing associations through an aggregate root

I have a Class struct which contains a set of Topics. I am
trying to treat Class as an Aggregate root in DDD parlance. Which
means all updates to sub-entities of Class (Topics in this case)
have to be managed by the Class. This is to maintain invariants within
the Class and provide transactional consistency within Class and its
sub entities.

Here is an outline of what I have:

def changeset(class, attrs) do
  class
  |> cast(attrs, [:name,...])
  |> cast_assoc(:class_topics)
  |> validate_required([:name, ...])
end
def create_topic(%Class{} = class, attrs \\ %{}) do
  number = length(class.topics) + 1

  new_topic =
    class.topics
    |> Enum.concat([Map.put(attrs, "number", number)])

  result = class
  |> Class.changeset(%{topics: new_topic})
  |> Repo.update()
end

This complains about cast_assoc requiring Maps (whereas in this case
Topic structs are supplied)

So I updated it to this:

def create_topic(%Class{} = class, attrs \\ %{}) do
  number = length(class.topics) + 1

  new_topic =
    class.topics
    |> Enum.map(fn i -> %{id: i.id} end)
    |> Enum.concat([Map.put(attrs, "number", number)])

  result = class
  |> Class.changeset(%{topics: new_topic})
  |> Repo.update()
end

Which also doesn’t achieve what I want.

I am doing it like this as there are additional constraints on the topics
being created - which prevents me from just creating a topic without first
preloading and validating against existing topics. (For example, I want
to ensure that there is at most 1 Topic which has a particular category
in a Class - which requires me to have them all available to validate this
when creating or updating a topic.)

I think there is something obvious I am not understanding - so I thought
I’d post the question here.

Any help would be appreciated.

First Post!

aseigo

aseigo

The code seems to be trying too hard to literally reflect the design concept you have in mind, by literally updating the Class db entry in order to create a new Topic row. Storage != responsibility.

It is entirely sensible to have create_topic in the Class module (I often do this myself as well for exclusively-owned associated data, for similar reasons as you are doing here), but trying to mimic this with storage layer interaction is a step too far imho.

Just do the validation in create_topic/2 and then .. well .. create a new Topic row in the db using Topic.changeset/2 and Repo.insert. Easy-peasy, and you’ll not need to work around Ecto so much, let alone have those Enum.map/2/Enum.concat/2 chains.

Last Post!

aseigo

aseigo

You would need to do such a lock anyways, since as it is written now you could end up in the same situation. create_topic could be called from multiple processes as it is!

But .. this is what databases are for: ACID. I would personally put such a constraint check in the database itself, either as a constraint on the relevant fields, or as a function used as a constraint trigger. Putting constraints that require global consistency in the application layer is rarely worth it. This would also obviate the need to fetch the current topics (with a lock, no less): the topic number could be set and the constraints checked in the database itself, and it would be done with guaranteed consistency. So, better guarantees and fewer roundtrips to the database. Constraint violations are returned as errors from Ecto, so you would be able to detect the violations in the application code still.

Where Next?

Popular in Questions Top

vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New
RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New

Other popular topics Top

KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36654 110
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 49084 226
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement