This is a sample repository showing how we can use pure domain models with SQLAlchemy in python.
- Create a virtual environment by running
python -m venv venv - Install the dependencies by running
pip install -r requirements.txt - Run tests using your IDE or
python -m pytest
We want to keep our domains as free of dependencies as possible. This enables us to let our domain specific code live on for ages, disregarding the current buzz in the developer space. Frameworks come and go, but our code needs to live on.
This example shows how to keep a completely pure python domain entity, while still using SQLAlchemy to persist and query a database for the entity.
Here's the pure class:
class Student():
"""
A Student is the main logic class of our system.
It's important to keep it as readable, maintainable and free of dependencies as possible.
"""
def __init__(self, student_number, first_name, last_name, year):
self.student_number = student_number
self.first_name = first_name
self.last_name = last_name
self.year = year
def greet(self):
"""
:return: An iunformal greeting of the student
"""
return f"Hi, {self.first_name} {self.last_name}"This class has absolutely no external dependencies and should be compatible with python 20.
It stands as a clear counterpart to the followin Django-bound entity; that has a parameterless constructor
imposed upon it in favor of a static factory method, and each field is initialized using
tightly coupled factory methods for metadata and value instances.
It clearly violates all of the SOLID principles all in one nice package.
from django.db import models
class Student(models.Model):
student_number = models.CharField(max_length=10)
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
year = models.PositiveIntegerField()
photo = models.ImageField(upload_to='student_photos/', blank=True, null=True)
def greet(self):
return f"Hi, {self.first_name} {self.last_name}"
@classmethod
def create_student(cls, student_number, first_name, last_name, year):
student = cls(student_number=student_number, first_name=first_name, last_name=last_name, year=year)
return studentGranted there are valuable information in the django-bound class like last names having a maximum length of 30. However, those rules are frequently very use-case and customer governed. A last name in the UK has totally different qualities than a last name in India or other parts of Asia. By moving these rules into metadata specific classes per solution we can leverage a larger flexibility. Here's an example of moving the metadata out of the entity:
student_table = Table(
'students',
cls.metadata,
Column('student_number', String, primary_key=True),
Column('first_name', String(50)),
Column('last_name', String(50)),
Column('year', Integer, CheckConstraint('year >= 1 '))
)
registry().map_imperatively(Student, student_table, primary_key=student_table.c.student_number)Traditional misunderstandings around the difficulties of "onion architecture" and "loosesly coupled ORM usage" involve
- You'll need a method per query on entity-specific repositories
- You'll never replace your persistence mechanism
- The built-in classes already implement UnitOfWork and Repository
This example shows that the effort is no bigger and that the generalizations leave our freedom in place, except our freedom is bigger since we can replace each individual class' persistence mechanisms on a whim. Provided of course our mechanism supports the five Repository methods: create, read, update, delete and query.