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
Top comments (1)
This has really expanded my understanding of the concept of CQRS! I’ve always assumed that you need to have separate database models to represent the command and query interfaces, but I can see value in this step along the way.