jmaniex

jmaniex

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.

Showing Posts 1 to 3

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.

jmaniex

jmaniex OP

Thanks @aseigo - I think I understand what you are saying.

So to do that I would need to read/preload the topics, do the validation/calculate derived data and create/update the new Topic record all inside of a transaction. Holding a lock on the Class row to ensure that there is not another concurrent update to the list of Topics associated with the class - which could break my desired invariants.

Does that sound right?

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.

— All posts loaded —

Where Next? Top

Trending in Questions Top

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews