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: https://lostkobrakai.svbtle.com/ecto-when-to-cast

Where Next?

Popular in Questions Top

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
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
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
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
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
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
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
Codball
Mix format works fine if run from the cmd. I’ve followed this to facilitate the implementation into VSC which involves downloading an ext...
New

Other popular topics Top

William
I would like to know that is there any online source for learning Phoenix Framework for building E-Commerce Store? Any advantage on build...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
polypush135
As many of you may have realized by now (sorry for all the posts here) I’ve been working on a db problem where I’m trying to aggregate a ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
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
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement