Repository
The Repository pattern is a design pattern that mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects. It centralizes data access logic, promoting a cleaner separation of concerns and making the application easier to test and maintain.
In CQL (Crystal Query Language), while the Active Record pattern is a prominent feature, CQL's design is flexible enough to support the Repository pattern. This allows developers to abstract the underlying data persistence mechanisms further and work with domain objects in a more decoupled manner.
Key Concepts of the Repository Pattern
Abstraction of Data Access: The repository provides an abstraction layer over data storage. Consumers of the repository work with domain objects and are unaware of how those objects are persisted or retrieved.
Collection-Like Interface: Repositories often expose methods that resemble collection operations, such as
add
,remove
,find
,all
, etc.Centralized Query Logic: Queries related to a specific aggregate root or entity are encapsulated within its repository.
Decoupling: It decouples the domain model from the data access concerns, improving testability and maintainability.
Unit of Work (Optional but Common): Repositories are often used in conjunction with a Unit of Work pattern to manage transactions and track changes to objects.
Implementing the Repository Pattern with CQL
CQL provides a generic CQL::Repository(T, Pk)
class that can be used as a base for creating specific repositories for your domain entities.
1. Define a Model (Entity)
First, you need a model (often a plain Crystal struct or class) that represents your domain entity. Unlike Active Record models, these entities typically don't include persistence logic themselves.
2. Create a Specific Repository
You create a repository for your entity by inheriting from CQL::Repository(T, Pk)
, where T
is your entity type and Pk
is the type of its primary key.
Explanation:
UserRepository < CQL::Repository(User, Int32)
: Defines a repository forUser
entities where the primary key isInt32
.initialize(schema : CQL::Schema = MySchema, table_name : Symbol = :users)
: The constructor expects aCQL::Schema
instance and the symbol representing the database table name (e.g.,:users
).super(schema, table_name)
: Calls the constructor of the baseCQL::Repository
class.find_active_users
,find_by_email_domain
: Custom methods that encapsulate specific query logic using CQL's query builder (query.where(...)
). Thequery
method is provided by the baseCQL::Repository
.
3. Using the Repository
Once defined, you can use the repository to interact with your data:
Methods Provided by CQL::Repository(T, Pk)
CQL::Repository(T, Pk)
The base CQL::Repository
class provides several convenient methods for common data operations:
query
: Returns a newCQL::Query
instance scoped to the repository's table.insert
: Returns a newCQL::Insert
instance scoped to the repository's table.update
: Returns a newCQL::Update
instance scoped to the repository's table.delete
: Returns a newCQL::Delete
instance scoped to the repository's table.build(attrs : Hash(Symbol, DB::Any)) : T
: Instantiates a new entityT
with the given attributes (does not persist).all : Array(T)
: Fetches all records.find(id : Pk) : T?
: Finds a record by its primary key, returnsnil
if not found.find!(id : Pk) : T
: Finds a record by its primary key, raisesDB::NoResultsError
if not found.**
find_by(**fields) : T?
**: Finds the first record matching the given field-value pairs.**
find_all_by(**fields) : Array(T)
**: Finds all records matching the given field-value pairs.create(attrs : Hash(Symbol, DB::Any)) : Pk
: Creates a new record with the given attributes and returns its primary key.**
create(**fields) : Pk
**: Creates a new record with the given attributes (using named arguments) and returns its primary key.update(id : Pk, attrs : Hash(Symbol, DB::Any))
: Updates the record with the givenid
using the provided attributes.update(id : Pk, **fields)
: Updates the record with the givenid
using the provided attributes (named arguments).update_by(where_attrs : Hash(Symbol, DB::Any), update_attrs : Hash(Symbol, DB::Any))
: Updates records matchingwhere_attrs
withupdate_attrs
.update_all(attrs : Hash(Symbol, DB::Any))
: Updates all records in the table with the given attributes.delete(id : Pk)
: Deletes the record with the given primary key.**
delete_by(**fields)
**: Deletes records matching the given field-value pairs.delete_all
: Deletes all records in the table.count : Int64
: Returns the total number of records in the table.**
exists?(**fields) : Bool
**: Checks if any records exist matching the given field-value pairs.
Advantages of Using the Repository Pattern with CQL
Improved Testability: You can easily mock repositories in your tests, isolating your domain logic from the database.
Centralized Data Access Logic: Keeps data access concerns separate from your domain entities and application services.
Flexibility: While
CQL::Repository
provides a good starting point, you can customize it or even write your own repository implementations if needed, without altering your domain models significantly.Clearer Intent: Queries are named and reside within the repository, making their purpose more explicit.
When to Use the Repository Pattern
In larger applications where a clear separation between domain logic and data access is crucial.
When you need to support multiple data sources or switch data storage technologies with minimal impact on the domain layer.
When your querying logic becomes complex and you want to encapsulate it.
To improve the testability of your application by allowing easier mocking of data access.
While Active Record can be simpler for straightforward CRUD operations, the Repository pattern offers more structure and decoupling for complex applications, and CQL provides the tools to implement it effectively.
Last updated
Was this helpful?