Repository Pattern

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

  1. 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.

  2. Collection-Like Interface: Repositories often expose methods that resemble collection operations, such as add, remove, find, all, etc.

  3. Centralized Query Logic: Queries related to a specific aggregate root or entity are encapsulated within its repository.

  4. Decoupling: It decouples the domain model from the data access concerns, improving testability and maintainability.

  5. 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.

# Example: A simple User entity
struct User
  property id : Int32?
  property name : String
  property email : String

  def initialize(@name : String, @email : String, @id : Int32? = nil)
  end
end

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 for User entities where the primary key is Int32.

  • initialize(schema : CQL::Schema = MySchema, table_name : Symbol = :users): The constructor expects a CQL::Schema instance and the symbol representing the database table name (e.g., :users).

  • super(schema, table_name): Calls the constructor of the base CQL::Repository class.

  • find_active_users, find_by_email_domain: Custom methods that encapsulate specific query logic using CQL's query builder (query.where(...)). The query method is provided by the base CQL::Repository.

3. Using the Repository

Once defined, you can use the repository to interact with your data:

Methods Provided by CQL::Repository(T, Pk)

The base CQL::Repository class provides several convenient methods for common data operations:

  • query: Returns a new CQL::Query instance scoped to the repository's table.

  • insert: Returns a new CQL::Insert instance scoped to the repository's table.

  • update: Returns a new CQL::Update instance scoped to the repository's table.

  • delete: Returns a new CQL::Delete instance scoped to the repository's table.

  • build(attrs : Hash(Symbol, DB::Any)) : T: Instantiates a new entity T with the given attributes (does not persist).

  • all : Array(T): Fetches all records.

  • find(id : Pk) : T?: Finds a record by its primary key, returns nil if not found.

  • find!(id : Pk) : T: Finds a record by its primary key, raises DB::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 given id using the provided attributes.

  • update(id : Pk, **fields): Updates the record with the given id using the provided attributes (named arguments).

  • update_by(where_attrs : Hash(Symbol, DB::Any), update_attrs : Hash(Symbol, DB::Any)): Updates records matching where_attrs with update_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?