blaix / woma

A python web development framework
MIT License
5 stars 0 forks source link

Persistence #8

Open blaix opened 8 years ago

blaix commented 8 years ago

Use the Entity/Repository pattern. An entity is any python object that can be persisted. A repository handles the persistence details for the entity.

Use SQL Alchemy to power repositories?

Repositories should be configured with an engine. I would like to make this explicit rather than depend on configuration. This will make it more flexible for situations where you want different dbs in different environments, or for different entities. For example:

from woma.repositories import create_engine, Repository

class MyAppRepo(Repository):
    engine = create_engine('sqlite:///:memory:')

An entity can be any python object, but woma should provide a convenience mixin for the usual style. For example:

from woma.entities import EntityMixin

class User(EntityMixin):
    fields = ('id', 'name', 'email')

# should be equivalent to:

class User(object):
    def __init__(self, id=None, name=None, email=None):
        self.id = id
        self.name = name
        self.email = email

It should be that simple. I want the freedom to manage my entity's attributes however I want (e.g. custom getters/setters) without worrying about stepping on the toes of the database mappings.

Each entity should have a repository that maps fields to the database:

from my_app.entities import User
from woma.repositories import fields, mapping

class UserRepo(MyAppRepo):
    entity = User
    table = 'users'

    # map db fields to entity attributes:
    fields = mapping(
        id=fields.integer(auto_increment=True, primary_key=True),
        fullname=fields.string(source='name'),
        email=fields.string() # source defaults to field name
    )

user = User.new(name='Justin Blake', email='justin@blaix.com')
user = UserRepo.create(user)
user.id # => 1
user = UserRepo.get(1)
user.name # => 'Justin Blake'

Note: If the default entity is dict, we can create repos for pure data.

Repository should provide standard methods for create, update, delete and getting all records or one record by primary key. For more complex queries, a query function could be provided that accepts a repository. It should not be exposed as part of the public interface of a Repository. I want to encourage defining explicit methods for repo queries. For example:

from woma.repositories import query

class UserRepo(MyAppRepo):
    ...
    @classmethod
    def get_by_email(cls, email):
        data = query(cls).filter_by(email=email).first()
        return cls.entity(**data) # not sure about making this explicit?

Making query a function and not a method on Repository allows running ad-hoc queries in places where that might be needed like the shell and your tests, without requiring adding a method or exposing a huge query interface on every repo. (maybe?)

Associations could be handled like this:

...

class Post(EntityMixin):
    fields = ('id', 'title', 'body', 'comments')

class Comment(EntityMixin):
    field = ('id', 'title', 'body', 'post_id')

class CommentRepo(EntityMixin):
    ...

class PostRepo(MyRepo):
    ...
    fields = mapping(
        ...
        comments=fields.has_many(CommentRepo)
    )

That could run into problems if we want Comment to a have an actual post field instead of just a post_id (would require circular imports) but maybe we can provide a module path to the repo as a string instead of the actual class.

blaix commented 8 years ago

When I say sql alchemy, I mean specifically the core: http://docs.sqlalchemy.org/en/latest/core/ - not the ORM

blaix commented 7 years ago

Consider rom-rb for inspiration: https://www.youtube.com/watch?v=9TBvWRgll64

blaix commented 7 years ago

A better source of inspiration is Ecto: https://hexdocs.pm/ecto/Ecto.html

I like the repo/schema/changeset/query breakdown.