Barna
Extends your Ecto schemas with convenience functions so that you can focus on your domain logic instead of plumbing.
Installation
The package can be installed by adding barna
to your list of dependencies in mix.exs
:
def deps do
[
{:barna, "~> 0.3"}
]
end
Adds the following functions
fetch/1
Allows you to get zero or one entry from the database. It supports fetching by one or more schema attributes and efficiently joining and preloading (non-nested) associations.
Examples
# Get by id
MyApp.Accounts.User.fetch(by: "07949870-7d31-48cc-8883-ce882760759f")
# > {:ok, %User{id: "07949870-7d31-48cc-8883-ce882760759f", ...}}
# Get by name AND age
MyApp.Accounts.User.fetch(by: [name: "John", age: 33])
# > {:ok, %User{name: "John", age: 33, ...}}
# If not found, it returns an error tuple
MyApp.Accounts.User.fetch(by: [email: "non-existing@example.com"])
# > {:error, :not_found}
# Get by id and include the Foo association (via left join)
MyApp.Accounts.User.fetch(by: "07949870-7d31-48cc-8883-ce882760759f", include: [:foo])
# > {:ok, %User{id: "07949870-7d31-48cc-8883-ce882760759f", foo: %Foo{bar: "baz"}}}
# Get by id and include the Foo association (via inner join)
MyApp.Accounts.User.fetch(by: "07949870-7d31-48cc-8883-ce882760759f", include!: [:foo])
# > {:ok, %User{id: "07949870-7d31-48cc-8883-ce882760759f", foo: %Foo{bar: "baz"}}}
# Return just the struct or nil (like Repo.one)
MyApp.Accounts.User.fetch(by: "07949870-7d31-48cc-8883-ce882760759f", result_as_tuple: false)
# > %User{id: "07949870-7d31-48cc-8883-ce882760759f"}
# or
# > nil
Note: the include
and include!
work via DB joins so everything is just 1 SELECT
SQL query.
SELECT u0."id", u0."email", u0."name", u0."inserted_at", u0."updated_at", f1."bar", f1."inserted_at", f1."updated_at" FROM "users" AS u0 LEFT OUTER JOIN "foos" AS f1 ON f1."user_id" = u0."id" WHERE (TRUE AND (u0."id" = $1)) [<<7, 148, 152, 112, 125, 49, 72, 204, 136, 131, 206, 136, 39, 96, 117, 159>>]
list/1
Allows you to get zero or more entries from the database. It supports matching by one or more schema attributes and efficiently joining and preloading (non-nested) associations.
Examples
# Get all entries
MyApp.Accounts.User.list()
# > [%User{id: "07949870-7d31-48cc-8883-ce882760759f", ...}, ...]
# Search by name AND age
MyApp.Accounts.User.list(by: [name: "John", age: 33])
# > [%User{id: 1, name: "John", age: 33, ...}, %User{id: 2, name: "John", age: 33, ...}, ...]
# If no entries are found it returns an empty list
MyApp.Accounts.User.list(by: [email: "non-existing@example.com"])
# > []
# Include the Foo association (via left join)
MyApp.Accounts.User.list(include: [:foo])
# > [%User{id: 1, foo: %Foo{bar: "baz"}}, %User{id: 2, foo: nil}, ...]
# Include the Foo association (via inner join)
MyApp.Accounts.User.list(include!: [:foo])
# > [%User{id: 1, foo: %Foo{bar: "baz"}}]
# Supports Ecto-style order_by
MyApp.Accounts.User.list(order_by: [desc: :name, asc: :id])
# > [%User{id: 2, name: "Liam"}, %User{id: 3, name: "Liam"}, %User{id: 1, name: "Anthony"}, ...]
# Another order_by example
MyApp.Accounts.User.list(order_by: :id) # orders by [asc: :id]
# > [%User{id: 1, name: "Anthony"}, %User{id: 2, name: "Liam"}, %User{id: 3, name: "Liam"}, ...]
# Supports limits
MyApp.Accounts.User.list(limit: 0)
# > []
Note: the default order_by is [asc: :inserted_at]
so it plays nicely with UUID-type IDs.
I thought I was supposed to use context modules for these kind of things
I found myself writing a lot of context modules that look like this:
defmodule MyApp.Accounts do
def fetch_user(id) do
User
|> where([u], u.id == ^id)
|> join(:left, [u], _ in assoc(u, :foo_asoc))
|> preload([_, fa], [foo_asoc: fa])
|> Repo.one
end
def fetch_user_by_email(email) do
Repo.get_by(User, email: email)
end
# ...
end
These are relatively small and simple functions but you still have to test them. You DO test your functions, right? The number of tests rises exponentially when you add a happy path and a few sad/error paths. Barna takes care of these simple functions for you.
If your functions need to do something more than a basic select query you should still write a custom function in your context module and use that.