Decoratex, load data into Ecto virtual fields

Hi everyone,

Decoratex is my first Elixir package. It’s a very simple package but it helped me and my team to organize some logic of our project.

It provides methods to add virtual fields in Ecto models and it loads data into them when needed, keeping the Ecto model structure. It’s meant to be used for fields that can’t be calculated with just DB queries.

In this article, we explain our motivation in depth.

Here is an example of use:

defmodule User do
  use Ecto.Schema
  use Decoratex

  decorations do
    decorate_field :ability, :string, &UserHelper.get_ability/1
    decorate_field :absences, :integer, &UserHelper.count_absences/1
    ...
  end

  schema "users" do
    has_many :periods, Period, on_delete: :delete_all
    belongs_to :setting, Setting
    field :email, :string
    ...
    add_decorations
  end
end

Then, you can load data into fields with decorate:

user = User 
|> Repo.get(1) 
|> Repo.preload(:settings) 
|> Repo.preload(:periods)
|> User.decorate
%User{email: …, ability: :manager, absences: 10}
# or User.decorate(:ability)
# or User.decorate([:absences, :ability])

And use the %User{} struct with extra data as you wish, for example in patttern matching, views, json…

As my frist package, I will appreciate any comments or suggestions :slight_smile:

In upcoming versions, I will try to add some improvements like sending custom params to the decoration functions.

Thank you!

12 Likes

If I put as decoration some data from a database, for example, some complex join query. Is there possibility to preload it for all elements, something like eager loading not to ping database every time when I call some decoration for any model in list for example?

3 Likes

Hi krapans,

I though on something like that, but I finally prefeered to keep all DB access outside the decorations so you can’t do a query in an unexpected place.

For example, we are trying to focus all the DB access in controllers, so we only use Repo there.
We have some modules to define queries where we do the eager loading and we use them only in controllers:

user = User
|> UserQuery.with_setting
|> UserQuery.with_periods
|> Repo.get
|> User.decorate(:ability)

And you can still decorating the user in other points when you need.

@attributes ~W(email ... absences)a

def render("show.json", %{user: user}) do
  render "show.json", user: User.decorate(user, :absences)
end

Also, we use pattern matching in the decorate functions to force us to use de appropriate queries in each case:

def get_ability(%User{setting: %Setting{} = setting} = user) do
  ...
end

So you must preload all needed data before any decoration.

I hope I understood your question, maybe you have another point of view or an idea about this so I will be glad to read about it :slight_smile:

4 Likes

Hi, I like the idea of this package! I’m trying to use it to use/display a virtual field, but I am getting an error. I think it is a problem with how I am passing the function to calculate the field in the decorate_field definition. What I am trying to do is have a display number that adds some letters and numeric padding to the id of the order record (i.e. order # 50 -> ABC00050). Here is my code:

defmodule Project.Order do
use Ecto.Schema
use Decoratex.Schema
import Ecto.Changeset

decorations do
decorate_field :order_number, :any, &(Order.assign_order_number/1)
end

schema “orders” do

decorations()

end

def assign_order_number(%Order{} = order) do
%{order | order_number: “ABC” <> format_order_number(order.order_id)}
end

def format_order_number(order.order_id) do
order.order_id
|> Integer.to_string
|> String.pad_leading(5,“0”)
end
end

Hi @tom-gerken, sorry for the late response, I totally miss the forum notification :-(.

You should pass an anonymous function and I think that your definition is not exactly right, it should be

&Order.assign_order_number/1

Or

&(Order.assign_order_number(&1))

I hope you figured this out at time :sweat_smile:.

Thank you for use the package :-)!