Defining Relations
Relations are the core concept in Decrel. They describe how different entities in your domain model connect to each other. This guide explains how to define and use relations effectively.
Basic Relation Types
Decrel provides three primary types of relations:
1. Single Relations (Relation.Single
)
Use this when one entity relates to exactly one other entity.
import decrel.Relation
// An employee has exactly one department
object Employee {
object department extends Relation.Single[Employee, Department]
}
2. Optional Relations (Relation.Optional
)
Use this when an entity may or may not relate to another entity.
import decrel.Relation
// An employee may have a manager (or not)
object Employee {
object manager extends Relation.Optional[Employee, Employee]
}
3. Many Relations (Relation.Many
)
Use this when one entity relates to multiple entities of the same type.
import decrel.Relation
// A department has multiple employees
object Department {
object employees extends Relation.Many[Department, List, Employee]
}
Composing Relations
Relations can be combined to express complex access patterns:
Sequential Composition (<>:
)
To follow one relation and then another:
import decrel.syntax.relation._
// Get the manager of an employee's department
val departmentManager = Employee.department <>: Department.manager
Parallel Composition (&
)
To retrieve multiple related entities at once:
import decrel.syntax.relation._
// Get both the manager and employees of a department
val departmentInfo = Department.manager & Department.employees
Combined Composition
You can combine these operators to create complex queries:
import decrel.syntax.relation._
// Get the department of an employee, along with that department's manager
// and all employees in that department
val employeeDepartmentInfo = Employee.department <>: (Department.manager & Department.employees)
Implementing Relations
To use relations for data access, you need to implement them based on your data source:
With ZQuery
import decrel.reify.zquery
import zio._
// Create a module with your implementation
object EmployeeRelations extends zquery[Any] {
implicit val employeeDepartmentProof: Proof.Single[Employee.department.type, Employee, Nothing, Department] =
implementSingleDatasource(Employee.department) { employeeIds =>
// Implementation to fetch departments for employees
ZIO.succeed(
employeeIds.map(id => id -> Department(s"Dept-${id.value}"))
)
}
// Other relation implementations...
}
With Fetch (cats-effect)
import decrel.reify.fetch
import cats.effect.IO
// Create a module with your implementation
object EmployeeRelations extends fetch[IO] {
implicit val employeeDepartmentProof: Proof.Single[Employee.department.type, Employee, Department] =
implementSingleDatasource(Employee.department) { employeeIds =>
// Implementation to fetch departments for employees
IO.pure(
employeeIds.map(id => id -> Department(s"Dept-${id.value}"))
)
}
// Other relation implementations...
}
Advanced Patterns
Contramap
Sometimes you need to adapt existing relations to work with different input types:
import decrel.Relation
// Existing relation
object Department {
object manager extends Relation.Single[Department, Employee]
}
// Adapt to work with department ID instead of department object
val departmentIdToManager = contramapOneProof(
departmentManagerProof,
Department.manager,
(id: Department.Id) => Department(id)
)
Custom Relations
For specialized cases, you can create custom relations:
import decrel.Relation
val customRelation = Relation.Custom(
// Reference to existing relation or custom implementation
)
Best Practices
- Domain-Driven Design: Define relations that match your domain model's natural language
- Composition Over Implementation: Focus on composing relations first, implement later
- Batch Fetching: Always implement batch fetching to avoid N+1 problems
- Single Responsibility: Each relation should represent one clear relationship
Next Steps
After defining your relations, you'll want to:
- Implement data access using ZQuery or Fetch
- Use your relations in your application code
- Consider adding automated testing with ScalaCheck or ZIO Test