Object-oriented – Business logic outside shared ORM models

object-orientedorm

I have a set of ORM models that are shared between the main business application and a couple minor side applications (such as an administrative web interface).

I don't want to put the object's business logic for the main application inside the ORM model classes because the web interface doesn't use any of it (and it would be too bloated).

That leaves me with the problem of having two classes for every real "object" (the business layer class and the ORM model class), and wondering how I should link the two. Either composition or inheritance would work, but both feel wrong. For example, I have a User class and a DBUser ORM model class. User "is not" a DBUser and a User "does not have" a DBUser.

Is there a standard solution or best practice to address this predicament? Or is this a case where there is no great answer and I just have to pick the one that makes me the least uneasy?

Here is a code example, just in case the above wasn't clear:

class DBUser(SQAlchemyBase):

    __tablename__ = 'users'

   user_id = Column(Integer, primary_key=True)
   username = Column(String, nullable=False)
   # ...


class User(object):

    def __init__(self, user_id):
        self.dbuser = db.query(DBUser).filter(DBUser.user_id == user_id).first()

    @property
    def username(self):
        return self.dbuser.username

    @username.setter
    def username(self, username):
        self.dbuser.username = username

    def connect_to_server(self, server):
        ...

    def save(self):
        db.add(self.dbuser)
        db.commit()
        db.detach(self.dbuser)

    def disconnect_from_server(self):
        ...

    def handle_incoming_action(self, action):
        ...

Best Answer

I don't know your ORM, but what you have looks like Data Transfer Objects, or DTOs: basically just data recordsets mapped into objects, with little to no further logic in them (here: DBUser). It is possible to access their data values from the business model. In best case, through an interface, so you can possibly use different data access layers.

Dependent on the ORM, one can also use the business objects directly for data mapping (as with (N)Hibernate in .NET/Java). It would be bad to implement separate code to copy data between DTOs and business object fields/properties, e.g. as part of load/save/update routines (see below)!

I see a serious design mistake in your business model, which occurs frequently by ORM novices (there's probably still a chance to correct it now):

Having Load (or initializer by ID) and Save functions on individual objects, even a DB transaction Commit. In most ORM, entities should be designed Persistence Ignorant, meaning that objects should have no code that is DB persistence related, especially not for individual objects, no attempts to control DB access and no properties for transient (not persisted), dirty (changed) or stale (changed in DB meanwhile).

Instead, ORM usually have a Unit of Work (I saw in a short Google search that it's also called "session" in SQAlchemy). The application just works with the entities within an active session, the session tracks changes and persists them all in the end (Flush/Commit, different settings possible).

Session function names like Load, Save or Update are often misleading, because they don't mean actual DB Insert, Update, Delete, Select statements for entities. These will be only scheduled and occur when the ORM decides it's the right time. In (N)Hibernate, changes will be persisted even without any Save/Update call on the entities, and unchanged entities will not be updated at all, even after Save/Update.

With attempts to force the ORM to individual CRUD statements to the DB, the user violates the ORM and destroys many helpful features!

It will also result in a huge number of individual DB roundtrips for single statements (called SELECT N+1 when traversing object graphs), instead of optimized queries and batches by the ORM.

Maintaining object/data identity:

Also, if using wrapping business objects, accessing DTOs, it should be ensured that object instances are unique per data row, just like the DTO. ORM usually just allow one DTO instance per database source item, even if queried or used in multiple places.