arconautishche

arconautishche

Integrate a "pure functions" core with DB via Ecto

Hey guys,

I’m just starting with Elixir and I’m trying to create a small ‘core’ module to implement a logic of playing a game. I’m struggling with understanding whether/how I should use Ecto changesets as for input/return values for the functions in the ‘core’ module.

Ideally, I would like to make the ‘core’ unaware of the DB and of the caller’s needs. Right now I’m just going to use this module from a Phoenix context. But I’d like to implement a GenServer later for playing a game, using the same ‘core’ logic module.

This is what a function in that module can look like:

defmodule Game do
   def add_player_to_match(match, new_player) do
      # check if the new_player has already been added
      # add the new_player to match.players and return ???
      ...
   end
end

This function will be called by a module that will retrieve an existing ‘Match’ from the DB and save the results (match with a player added) back to the DB.

Is it ok for this function to accept an Ecto struct? Is it a good pattern for this function to return an Ecto changeset with the applied changes? This makes it super easy to save the result to the DB, but it kinda feels awkward. Also, in this case the unit tests of this function need to assert on the changes in the changeset, instead of the ‘business’ data itself. And piping these ‘core’ functions wouldn’t be possible either.

If this is not a good way to go, would you make the “core” functions work on pure (untyped) maps? Would you convert the Ecto struct to a Map using Map.from_struct/1 before passing it into the function? The way I see it could maybe work is something like this:

match = Repo.get!(123)
player = Repo.get!(456)
updated_match = Game.add_player_to_match(
  match |> Map.from_struct,
  player |> Map.from_struct
)

match
|> Ecto.Changeset.change(updated_match)
|> Repo.update

I would really appreciate some advice and/or pointers to articles/blogs about this kind of patters.

P.S. In case it’s relevant, these are the simple schemas (irrelevant fields redacted) I have for validations and interacting with the DB:

defmodule Match do
  use Ecto.Schema
  import Ecto.Changeset

  schema "matches" do
    field :finish, :utc_datetime
    field :start, :utc_datetime

    has_many :players, Player
    ...
  end
defmodule Player do
  use Ecto.Schema
  import Ecto.Changeset

  schema "players" do
    field :current_score, :integer
    belongs_to :match, Match
    ...
  end

Most Liked

mindok

mindok

Here’s a post that may be somewhat relevant (probably less for the data structure discussion but more for the migrating to GenServer bit coming later): The Erlangelist - To spawn, or not to spawn?

There is something that sits between an Ecto schema and a map - an Elixir struct (see defstruct in the docs) - it give your data a more defined shape than a pure map but without binding to the persistence mechanism. An Ecto schema is really just an Elixir struct with some extra fruit.

Depending on the whole scope I would most likely build this kind of thing using elixir structs for most of the data types going in and out of the core functions. I may start out with plain maps and lists while I’m iterating, but prefer structs for the extra safety net they provide (e.g. you can use pattern matching to ensure your core functions are only called with the correct data structure). I’d make the core function work how I really want them to work without considering persistence. Then I’d look at persistence and if it just so happened that one of the structs looks more or less like the database table that would be used for persistence I might upgrade that struct to an Ecto schema. I wouldn’t generally return a changeset from a core function - the persistence functions should be responsible for converting what the core functional layer is doing into something that can be pushed into the database.

There’s a bit of a discussion about generating changesets from plain structs here:

There is a also a highly relevant discussion in “Designing Elixir Systems with OTP” (Chapter 9) - a Pragmatic Programmer title (so you can probably get a discount). This also includes a pattern for dependency injection of the persistence mechanism if you want to take decoupling layers to the next level.

LostKobrakai

LostKobrakai

This is by design. casting is meant to move external/user/not validated/weakly typed data into a known/broader typed/validated format. Such data is hardly ever found as structs, which at least prescribes a known format. For structs you can use any of the other functions in Ecto.Changeset, which apply changes without casting.

I’ve explained this a bit more in this blog post: Ecto: When to cast

Where Next?

Popular in Questions Top

Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
mgjohns61585
Could someone help me? I’m making my first elixir program, number guessing game. I can’t figure out how to convert the user’s guess from ...
New
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
vac
Hi, I’m quite new in Elixir and I’m trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and I...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New

Other popular topics Top

New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
AstonJ
Please see the new poll here: Which code editor or IDE do you use? (Poll) (2022 Edition) It’s been a while since we first asked this, I...
208 31142 143
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New

Latest on Elixir Forum

We're in Beta

About us Mission Statement