Repositories

Interface for using the Repository Design Pattern.

Instead of using the AbstractStore (or any of its implementations like Store) as a God Class the Repository Design Pattern allows you to follow the Single Responsibility Rule.

A Repository acts like an in-memory domain object collection, that connects the domain and data mapping layers. Objects can be easily added to and removed from the Repository, due to the mapping code of the Repository which will ensure that the right operations are executed behind the scenes.

Apart from acting as a bridge between the data and the domain layer, the Repository also provides some useful methods for storing, updating, deleting and querying data.

Examples

Lets assume that you are using the default experimentum data store Store. With Repository there is already an implementation which will handle the mapping between the Repository objects and the SQLAlchemy ORM which is used by the Store.

For simplicity we will assume that your database already contains a User and Address table (for more information on how to specify your domain layer, see: Migrations).

Data Layer

Each user has a name, fullname and a password field and can have multiple adresses. Each address has an email field:

from experimentum.Storage import AbstractRepository

class AddressRepository(AbstractRepository.implementation):
    __table__ = 'Address'

    def __init__(self, email):
        self.email = email


class UserRepository(AbstractRepository.implementation):
    __table__ = 'User'

    __relationships__ = {
        'addresses': [AddressRepository]
    }

    def __init__(self, name, fullname, password):
        self.name = name
        self.fullname = fullname
        self.password = password

In the __init__ method of the repository you specify the fields of the table. The __table__ attribute contains the name of the table, while the optional __relationships__ attribute specifies any relationships of the data. The key is the attribute under which the data will be accessible, ie:

print(user.addresses)  # will print all addresses of a specific user

Actions

There are several methods for querying data, like find(), get(), first(), all():

# Find a user by its id
user = UserRepository.find(2)  # get user with ID 2
print(user.id, user.name, user.fullname, user.addresses)

# Get many users by a condition (here where name != 'John')
users = UserRepository.get(where=['name', '!=', 'John'])
for user in users:
    print(user.id, user.name, user.fullname, user.addresses)

# Get the first user which satisfies a certain condition
user = UserRepository.first(where=['name', 'John'])
print(user.id, user.name, user.fullname, user.addresses)

# Get all users
users = UserRepository.all()
for user in users:
    print(user.id, user.name, user.fullname, user.addresses)

The Repository class also provides several methods for storing, updating, and deleting data, like create(), update() delete():

# Create a new user from a data dictionary
user = UserRepository.from_dict({
    'name': 'Hello',
    'fullname': 'World',
    'password': '1234',
    # Turns the entries into AddressRepository instances and adds them to the user repo
    # so that they get saved with the correct user id
    'addresses': [
        {'email': 'hello@world.com'},
        {'email': 'foo@world.com'},
    ]
})
user.create()  # create the new user

# Update a user
user = UserRepository.find(2)
user.fullname = 'Doe'
user.name = 'John'
user.update()

# Delete a user
user = UserRepository.find(1)
user.delete()

Events

A Repository provides several events, allowing you to hook into the following points in a repositorie’s lifecycle: before_insert(), after_insert(), before_update(), after_update().

Events allow you to easily execute code each time after/before a specific repository object is saved or updated in the data store.

class AddressRepository(AbstractRepository.implementation):
    # ...

    def after_insert(self):
        print('Gets called each time after an address is saved to the database.')
        print(self.id, self.email)  # Object has access to the inserted id

Loading

In order for the framework to map all the repositories it has to load them via the RepositoryLoader class. For the RepositoryLoader class to be able to find the repositories, the files have be in your repository folder and end with Repository.py. The repository files should contain a repository class with the same name as the file name.