Using CQRS in Phoenix Contexts

Recently I read this article Naming Phoenix context functions. And I think using naming conventions is good and all, but maybe we could make a step futher and apply CQRS inside such modules.

Command Query Responsability Segregation is the notion that…


This content originally appeared on DEV Community 👩‍💻👨‍💻 and was authored by Camilo

Recently I read this article Naming Phoenix context functions. And I think using naming conventions is good and all, but maybe we could make a step futher and apply CQRS inside such modules.

Command Query Responsability Segregation is the notion that you can use a different model to update information than the model you use to read information...
--- Martin Fowler

I made a small system to store gameplays results of dancing machines such a Pump it Up.

For example this is the cards.ex file.

defmodule Rankmode.Cards.Queries do
  import Ecto.Query, warn: false

  alias Rankmode.Repo
  alias Rankmode.Cards.Card

  def all() do
    Repo.all(Card)
    |> Repo.preload([:user, :game, :mix])
  end

  def for(user: user_id) do
    from(c in Card,
      where: c.user_id == ^user_id,
      order_by: [desc: c.activated_at],
      preload: [:user, :game, :mix, :profile])
    |> Repo.all()
  end

  def get!(uid: uid) do
    from(c in Card, where: c.uid == ^uid,
      preload: [:user, :game, :mix, :profile]
    )
    |> Repo.one!()
  end

  def get(uid: uid) do
    from(c in Card, where: c.uid == ^uid,
      preload: [:user, :game, :mix, :profile]
    )
    |> Repo.one()
  end

  def get(id: card_id, user: user_id) do
    from(c in Card,
      where: c.id == ^card_id and c.user_id == ^user_id,
      preload: [:user, :game, :mix, :profile]
    )
    |> Repo.one()
  end

  def get(:notactivated, uid: uid) do
    from(c in Card,
      where: c.uid == ^uid and
        is_nil(c.activated_at) and
        is_nil(c.user_id),
      preload: [:user, :game, :mix]
    )
    |> Repo.one()
  end
end

defmodule Rankmode.Cards.Changesets do
  alias Rankmode.Cards.Card

  def new(attrs) do
    %Card{}
    |> Card.changeset(attrs)
  end

  def empty() do
    new(%{})
  end

  def activate(id, attrs) do
    %Card{id: id}
    |> Card.changeset_activate(attrs)
  end

  def update(id, attrs) do
    %Card{id: id}
    |> Card.changeset(attrs)
  end
end

defmodule Rankmode.Cards.Commands do

  import Ecto.Query, warn: false

  alias Rankmode.Repo
  alias Rankmode.Cards.Changesets

  def create(attrs) do
    Changesets.new(attrs)
    |> Repo.insert()
  end

  def activate(id, attrs) do
    Changesets.activate(id, attrs)
    |> Repo.update()
  end

  def update(id, attrs) do
    Changesets.update(id, attrs)
    |> Repo.update()
  end
end

defmodule Rankmode.Cards do
end

And the corresponding Schema file card.ex

defmodule Rankmode.Cards.Card do
  use Ecto.Schema
  import Ecto.Changeset

  schema "cards" do
    field :uid, :string
    field :checksum, :string
    field :activated_at, :naive_datetime
    belongs_to :mix, Rankmode.Mixes.Mix
    belongs_to :game, Rankmode.Games.Game
    belongs_to :user, Rankmode.Accounts.User
    has_one :profile, Rankmode.Profiles.Profile
    timestamps()
  end

  @optional [:activated_at, :user_id, :mix_id, :game_id]
  @required [:uid, :checksum]

  def changeset(model, attrs) do
    model
    |> cast(transform(attrs), @optional ++ @required)
    |> validate_required(@required)
    |> validate_length(:uid, min: 3, max: 255)
    |> unique_constraint(:uid, name: :cards_uid_checksum_index)
    |> unique_constraint(:checksum, name: :cards_uid_checksum_index)
  end

  def changeset_activate(model, attrs) do
    changeset(model, activate(attrs))
  end

  defp checksum(attrs) do
    Map.merge(attrs, %{checksum: Base.encode16(:crypto.hash(:sha256, Map.get(attrs, :uid, "")))})
  end

  defp activate(attrs) do
    Map.merge(attrs, %{activated_at: NaiveDateTime.utc_now()})
  end

  defp transform(attrs) do
    checksum(attrs)
  end
end

My approach is separating the concerns in 4 modules.

Base Module

A base module, that could store helper functions or any other function that does not fit in the other modules.

defmodule Rankmode.Cards do
end

Queries Module

A module for mostly SELECT type functions

defmodule Rankmode.Cards.Queries do
end

Commands Module

A module for mostly INSERT, UPDATE, DELETE type functions.

defmodule Rankmode.Cards.Commands do
end

Changesets Module

A module for storing the changesets used both in Queries and Commands.

defmodule Rankmode.Cards.Changesets do
end

File structure

I prefer storing these in a single file. But if it becomes messy overtime, an structure like this can be used.

cards/
├── card.ex
├── cards.ex
├── changesets.ex
├── commands.ex
└── queries.ex


This content originally appeared on DEV Community 👩‍💻👨‍💻 and was authored by Camilo


Print Share Comment Cite Upload Translate Updates
APA

Camilo | Sciencx (2023-01-12T14:18:18+00:00) Using CQRS in Phoenix Contexts. Retrieved from https://www.scien.cx/2023/01/12/using-cqrs-in-phoenix-contexts/

MLA
" » Using CQRS in Phoenix Contexts." Camilo | Sciencx - Thursday January 12, 2023, https://www.scien.cx/2023/01/12/using-cqrs-in-phoenix-contexts/
HARVARD
Camilo | Sciencx Thursday January 12, 2023 » Using CQRS in Phoenix Contexts., viewed ,<https://www.scien.cx/2023/01/12/using-cqrs-in-phoenix-contexts/>
VANCOUVER
Camilo | Sciencx - » Using CQRS in Phoenix Contexts. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2023/01/12/using-cqrs-in-phoenix-contexts/
CHICAGO
" » Using CQRS in Phoenix Contexts." Camilo | Sciencx - Accessed . https://www.scien.cx/2023/01/12/using-cqrs-in-phoenix-contexts/
IEEE
" » Using CQRS in Phoenix Contexts." Camilo | Sciencx [Online]. Available: https://www.scien.cx/2023/01/12/using-cqrs-in-phoenix-contexts/. [Accessed: ]
rf:citation
» Using CQRS in Phoenix Contexts | Camilo | Sciencx | https://www.scien.cx/2023/01/12/using-cqrs-in-phoenix-contexts/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.