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

_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lists...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => ...
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

Other popular topics Top

TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 41989 114
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
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
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
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
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement