Introducing Drops.Relation: High-Level Relation Abstraction on top of Ecto
I'm excited to announce the latest addition to the Elixir Drops suite of libraries: Drops.Relation. This new library provides a high-level API for defining database relations with automatic schema inference and composable queries, simplifying database interactions when developing applications in Elixir.
Drops.Relation
is based on 10 years of my work on the Ruby Object Mapper project and brings the most powerful features of ROM to Elixir.
What is Drops.Relation?
Drops.Relation
bridges the gap between Ecto and application-level data handling and management. It automatically introspects your database tables, generates Ecto schemas, and provides a convenient query API that feels like working directly with Ecto.Repo
while adding powerful composition features.
Think of it as a smart wrapper around Ecto that eliminates boilerplate while adding sophisticated query composition capabilities, while preserving your full control over your relations and their schemas.
Getting Started
Add it to your mix.exs
:
def deps do
[
{:drops_relation, "~> 0.1.0"}
]
end
Configure it in your config.exs
:
config :my_app, :drops,
relation: [
repo: MyApp.Repo
]
Then run the installation task:
mix drops.relation.install
This should create aliases for ecto tasks that will ensure that schemas are refreshed whenever you run migrations.
To test it out, just run migrations:
mix ecto.migrate
If things go well, you will see this at the end of the output:
Cache refresh completed
From there, you can start defining your relations with inferred schemas ✨
Automatic Schema Inference
One of Drops.Relation
's standout features is automatic schema inference. Instead of manually defining every field, type, and constraint, you can let the library introspect your database:
defmodule MyApp.Users do
use Drops.Relation, otp_app: :my_app
schema("users", infer: true)
end
The library automatically discovers:
- Column names and types
- Primary and foreign keys
- Indexes and constraints
- Default values and nullability
You can access the generated schema programmatically:
schema = MyApp.Users.schema()
schema[:email]
# %Drops.Relation.Schema.Field{
# name: :email,
# type: :string,
# source: :email,
# meta: %{
# default: nil,
# index: true,
# type: :string,
# primary_key: false,
# foreign_key: false,
# nullable: false
# }
# }
Familiar Repository API
Drops.Relation
provides all the familiar Ecto.Repo
functions you're used to but makes them more accessible:
# Reading data
user = Users.get(1)
user = Users.get_by(email: "john@example.com")
users = Users.all()
users = Users.all_by(active: true)
# Writing data
{:ok, user} = Users.insert(%{name: "John", email: "john@example.com"})
{:ok, user} = Users.update(user, %{name: "Jane"})
{:ok, user} = Users.delete(user)
# Aggregations
count = Users.count()
avg_age = Users.aggregate(:avg, :age)
Composable Queries
Where Drops.Relation
really shines is in query composition. You can chain query operations together to build complex queries:
# Basic composition
active_users = Users
|> Users.restrict(active: true)
|> Users.order(:name)
|> Enum.to_list()
# Complex restrictions with multiple conditions
admins = Users
|> Users.restrict(role: ["admin", "super_admin"])
|> Users.restrict(active: true)
|> Users.order([{:last_login, :desc}, :name])
# Works seamlessly with Enum functions
user_names = Users
|> Users.restrict(active: true)
|> Enum.map(& &1.name)
In the first release Drops.Relation
provides 3 high-level query operations:
Drops.Relation.restrict
- filters data based on conditionsDrops.Relation.order
- sets orderingDrops.Relation.preload
- preloads associations
More operations will be added in future releases.
Custom Queries with defquery
For more complex scenarios, you can define reusable query functions using the defquery
macro:
defmodule MyApp.Users do
use Drops.Relation, otp_app: :my_app
schema("users", infer: true)
defquery active() do
from(u in relation(), where: u.active == true)
end
defquery by_role(role) when is_binary(role) do
from(u in relation(), where: u.role == ^role)
end
defquery by_role(roles) when is_list(roles) do
from(u in relation(), where: u.role in ^roles)
end
defquery recent(days \\ 7) do
cutoff = DateTime.utc_now() |> DateTime.add(-days, :day)
from(u in relation(), where: u.inserted_at >= ^cutoff)
end
defquery with_posts() do
from(u in relation(),
join: p in assoc(u, :posts),
distinct: u.id)
end
end
These custom queries are fully composable with built-in operations:
# Compose custom queries
recent_admins = Users
|> Users.active()
|> Users.by_role("admin")
|> Users.recent(30)
|> Users.order(:name)
|> Enum.to_list()
# Mix with restrict operations
active_users_with_email = Users
|> Users.active()
|> Users.restrict(email: {:not, nil})
|> Users.order(:email)
Advanced Feature: Boolean Query Logic
For complex query logic involving multiple conditions, Drops.Relation
provides a powerful query
macro with boolean operations:
import Drops.Relation.Query
# Simple AND operation
adult_active_users = Users
|> query([u], u.active() and u.adult())
|> Enum.to_list()
# Complex nested conditions
complex_query = Users
|> query([u],
(u.active() and u.adult()) or
(u.inactive() and u.with_email())
)
|> Users.order(:name)
|> Enum.to_list()
# Mix built-in and custom operations
filtered_users = Users
|> query([u],
u.active() and
u.restrict(role: ["admin", "user"])
)
|> Enum.to_list()
What's Next?
Future releases will include:
- Enhanced association handling
- Type-safe high-level query handling
- More composable query operations
- Integration with other Elixir Drops libraries
- Generators for Phoenix
Try It!
Drops.Relation
is available on Hex.pm and the source code is on GitHub.
The library is currently in its early stages, testing and feedback are welcome!
Give it a try and let me know what you think! I'm always interested in feedback and contributions from the community 💜