6

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?

2 Answers 2

3

If you really want to decouple your persistence layer from your domain logic, you could use plain old python objects as domain objects and the "models" only for persistence. You could encapsulate business logic in a service layer which has a repository (doing db things) as its dependency.

Within the service layer you would deal with the domain objects - in your case Food.

Food abstracts away FoodData Nutrient and NutrientGroup.

Food would then implement something like a get_values-method which gives an easy consumable dictionary back.

The repository deals with the models and the related db stuff. The repository methods take Food / collections of Food as parameter and return Food / collections of Food to the caller.

The clear upside is flexibility in terms of testability and changeability - e.g. if you later on switch the persistence layer.

The downside is the "double bookkeeping" since every change on a domain object's variables causes a change in the model.

1

I have used this pattern in the past with flask but I think there are a number of issues with your implementation; I think it's better to know the model directly from the class, as this enable you only import the model class once:

class BaseManager:
    model_type = None
    def __init__(self, model=None, **kwargs):
        # The base class keeps a reference of the orm model
        if model is None:
            # other init code from the create() class func can go here
            model = self.model_type(**kwargs)
        self.model = model

Next, the other class methods can be removed. In place of a get_food() class method, you can put a get_by_id in the base class:

 @classmethod
 def get_by_id(cls, id):
     # Get a model by its id
     # Similar to `__init__` method, but the ORM model comes from 
     # the db
     model = cls.model_type.query.filter_by(id=id).first()
     return cls(model)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.