Thoughts on the confusion around models

The topic of models have come up a lot lately, in conference talks, podcasts and here on the forum:

There seems to be some confusion about the subject. Being fairly new to Phoenix I certainly feel confused :slight_smile: I would like to start a discussion about the general concept of models to see if we can clear things up a bit.

In my view, some of the confusion comes from the fact that the word model is such an overloaded term, used to mean many different things. I like the way that the MVVM pattern calls this out: Model + View + ViewModel. There are (at least) two models!

I would say that most applications need three models. Three different models that is.

First, there is the Domain Model. This is made up of entities, value objects and rules from the application domain. It’s the DDD stuff we all know and love. It doesn’t know about databases or the web. It sits next to the business logic and is the core of your application.

Then comes the ViewModel / Presentation Model. This is a model of the graphical user interface state. This model holds some domain data as well as pagination data, search phrases and other things shown but not part of the domain model.

Finally there is the Persistence Model, or the database model. This is a model of how the domain data should be stored. I tend to think about this as the DB schema+views+triggers etc. It is commonly similar to the domain model but it doesn’t have to be. The domain model might be made up of entities while the persistence model is an event stream. The job of an ORM is to translate between domain model and persistence model.

So, at long last. How does this match the concepts of Phoenix/Ecto? I would love to hear your thoughts on this.

Is the View in Phoenix the same as a ViewModel in the MVVM pattern terminology? If so, is the phoenix template a MVVM View?

Are Ecto’s models a framework for building persistence models or are they trying to accomodate both domain and persistence models? How about changesets? Are the changesets (alone) what we should use to build our domain models and then use Ecto models as a persistence model for the changesets?

7 Likes

As of Ecto 2.0 models are known as schemas. They are not intended to be rich modules with business logic, just a tool that you use to retrieve data, perform validations and submit changes. You can even use Ecto 2.0 in a schema-less fashion and not define any schemas at all!

In the common case of simple CRUD applications, you can get pretty far with just letting the persistence model be the domain model. I find this particularly true because of the functional style of elixir. there are no spooky mutations happening to data as your app runs, just explicit transformations.

6 Likes

Agreed that there are many useful applications that only need a straight DM/DB passthrough. Schemas seems to be a great way of encoding “type information” about the domain entities.

One thing that still confuses me about Ecto is how aggregates are supposed to be handled. In DDD speak, aggregates are the units of transactional consistency. Often a single entity (Ecto model) is a complete aggregate. In my past (OO) experience however, the typical aggregate size is 1-5 entities.

If the standard way is to collapse the domain model and the persistence model, how do we handle aggregates with Ecto? Is the question even relevant or is there some deeper underlying design difference from OO style DDD that I’m not getting?

2 Likes

One simple convention to emulate aggregates is to:

  • Never build queries directly in controllers
  • Instead expose query-building functions from your aggregate root module.
  • Never build changesets in controllers or modules other than the aggregate root module
  • Instead the aggregate root module exposes *_changeset functions changing the aggregate as a whole.

For example, if you have aggregate SalesOrder with one-to-many relationship to SalesOrderItem, but you don’t want arbitrary code to update SalesOrderItem, the SalesOrder module exposes *_changeset functions for controlled updates to the SalesOrder aggregate.
The SalesOrderItem module however just declares a schema.

defmodule MyApp.SalesOrder do
  schema "sales_orders" do
    # ... fields 
    has_many :order_items, SalesOrderItem
  end

  # query to load by ID
  def by_id(id) do
    from order in MyApp.SalesOrder, where: id == ^id, preload: :order_items
  end

 # query for Orders containing a specific catalog item
 def with_catalog_item(catalog_item_id) do
   from order in MyApp.SalesOrder,
   join: item in assoc(order, :items),
   where: item.catalog_item_id == ^catalog_item_id,
   select: order,
   preload: items
 end

  # changeset for new Sales Order
  def new_changeset(params) do
    %SalesOrder{}
    |> cast(params, [:field1, :field2]
    |> cast_assoc(:items, with: &items_changeset/2)
    |> validate_required([:field1])
  end

  # private changeset for sales order items
  defp items_changeset(order_item, params) do
    order_item
    |> cast(params, [:catalog_item_id, :quantity, :price])
    |> validate_required([:catalog_item_id, :quantity, :price])
  end

  # Changeset to add an item to a sales order
  def add_item_changeset(order = %SalesOrder{}, params) do
      # ... cast params to SalesOrderItem and build changeset
  end

  # Changeset to remove an item to a sales order
  def remove_item_changeset(order = %SalesOrder{}, params) do
      # ... validate params and build changeset
  end
end
7 Likes

Seems like a good start. But there is still no transactional guarantees when modifying both SalesOrderItems and SalesOrders. Or am I missing some thing? Two processes might use the roots functions at the same time and try to issue writes.

The only way I see that one could get a strong guarantee on aggregate consistency is by implementing them as a genservers. Then write operations to the persistence layer can be guaranteed sequential and transactional. This might be a bit overkill, I don’t know?

2 Likes

Lightweight transactional guarantee is provided by Repo.transaction or Ecto.Multi.

Representing aggregates as processes is a nice approach of your going down the CQRS / event sourcing path.

Commanded looks promising, but certainly you’re taking on more complexity than a simple CRUD app.

5 Likes

I can recommend a read through of the Ecto 2.0 ebook: http://pages.plataformatec.com.br/ebook-whats-new-in-ecto-2-0

I’ve just finished it and it clarified the Ecto teams positions on some of these model questions.

One thing I find interesting is where they suggest using two schemas to separate gui data representation from a DB representation. It’s like using Ecto schemas to go between DB and domain model (struct) and also from domain model to presentation model (also struct).

4 Likes

Glad to read this :raised_hands:

We are structuring our application in a similar way. The only difference is that the query-building functions are exposed in their own module instead on the aggregate root module. Following your SalesOrder example, we are exposing the queries from a SalesOrder.Query module.

3 Likes

This is me looking at: Lonestar ElixirConf 2017- KEYNOTE: Phoenix 1.3 by Chris McCord

Much of the accumulated knowledge from the DDD world is now, finally, embraced by Phoenix. Even though Chris uses a bit different words, separating Blog and Sales is basically creating aggregates. They have roots (outside their directory) and the example even emphasis the transactional nature using Ecto.multi.

Using the word Bounded Context is a bit unfortunate though. It’s originally used (by Fowler and Evans) to separate complex domains, not to group small parts inside one domain. A bounded context in DDD is used to bound the semantics of the language used for different terms in the domain language. What @chrismccord describes is normally called aggregates. I feel like this will cause some confusion with people coming from a DDD background. If anything I would view separate Umbrellas as Bounded Contexts.

BUT, Ecto 2 and Phoenix 1.3 is definitely going in the right direction fast. I’m so happy about this!

Edit: Looking at this: Lonestar ElixirConf 2017 - KEYNOTE: Phoenix 1.3 by Chris McCord

I feel that Chris is actually using Bounded Context in the original sense. Perhaps I just missunderstood the talk.

2 Likes