I have a Flask application with tens of complex models, almost all of them related to each other.
A simple pseudo-schema of some of them:
+----------------+
| FoodGroup |
+-------+--------+
^
|
+-------+--------+
| Food +<------+
+---+------------+ | +--------------+
^ | | NutrientGroup|
| | +-------^------+
| | |
| | |
+---+----------------+ +---------------+ +------+-----+
| FoodMeasurements | | FoodData +------>+ Nutrient |
+--------------------+ +---------------+ +------------+
My target is to decouple as much as possible the business logic from the ORM models and encapsulate the data operations in a semantic and safe way. Many operations require a lot of logic by lots of models, and I want to avoid direct mixing of ORM models. First thought was to create a services module and fill it with functions as others suggested. I prefer a more OOP approach, so I implemented a Manager layer which will be responsible for the data operations.
eg I won't touch directly the FoodMeasurement. A FoodMeasurement belongs to Food. Similarly, FoodData belongs to Food too and has a Nutrient relationship. I need a FoodManager to alter their values. (At the real db schema, Food is related to 9 tables.)
At the following code I'll try to explain the concept. (Its not the original code, so pardon me for errors and typos.)
class BaseManager:
def __init__(self, model):
# The base class keeps a reference of the orm model
self.model = model
def save(self):
# code for saving obj to db
...
def create(self, **kwarg):
raise NotImplementedError()
# method which all children should implement
def get_id(self):
# similar methods will be on the children, like get_name etc
return self.model.id
class FoodManager(BaseManager):
def __init__(self, model: Food):
# When the class is initiated it passes the orm model to the
# model property
super().__init__(model)
@classmethod
def create(cls, name: str, food_id: int, food_group_id: int,
enabled: Optional[bool] = True):
# When `create` method is called it creates a FoodManager
# object with the ORM model assigned to the model property
args = locals()
args.pop("cls", None)
food = Food(**args)
return cls(food)
@classmethod
def get_food(cls, id):
# Get a food by its id
# Similar to `create` method, but the ORM model comes from
# the db
food = Food.query.filter_by(id=id).first()
return cls(food)
def get_measurements(self):
return self.model.measurements.all()
def get_nutrients(self):
return self.model.nutrients.all()
def set_measurements(self):
# lots of code, logic, validation etc...
self.model.measurements.append(data)
# for chaining
return self
def set_nutrients(self):
#similar to the above
self.model.nutrients.append(data)
return self
def update_measurements(self):
# ... checks for modified FoodMeasurement ORM models
# and acts accordingly ...
return self
# Now I can create a food and save it to db
food = FoodManager.create(name="Banana", food_id=1, food_group_id=1)
food.set_nutrients(data)
food.set_measurements(data_2)
food.save()
# I can do oneliners too
FoodManager.create(**food_data)
\ .set_nutrients(**nutrients).set_measurements(**meas).save()
# Furthermore, I can load a record from the db and do some operations
db_food = FoodManager.get_food(1)
db_food_meas = db_food.get_measurements()
# ... code for edit/remove/add measurements
db_food.update_measurements(db_food_meas).save()
# Finally, if I already have an orm model from another operation
orm_food = Food.query.filter_by(id=id).first()
etc_food = FoodManager(orm_food)
# ...
Do you recommend this pattern? Which flaws will I encounter using this pattern?