Friday, 24 October 2025

Persistence Ignorance

I've used SqlAlchemy in some projects (basic use, projects where the Database is just one of multiple datasources), and until recently I'v been sticking to using the Imperative mapping style. I grew up as a developer with Persistence Ignorance (PI) as a guiding principle (keep your domain model free from infrastructure concerns like database access, so it remains clean, testable, and focused on business logic), so that was the natural thing to me, and I was really surprised to see that SqlAlchemy recommends to use the Declarative mapping style, where the entities are totally aware of the specific persistence mechanism. .Net Entity Framework and NHibernate make a good job in allowing us to have entities that are "almost" persistent ignorant. I say "almost" cause if you check this list of things that go against Persistence Ignorance, you'll recognize some entity framework requirements like parameterless constructor and using virtual properties for lazy loaded relations. You can have all the additional constructors that make sense for your entities, EF just needs this parameterless one as it will initialize your entities by calling it and then setting properties as needed. As for the virtual properties, EF implements lazy-loading by means of creating proxy classes. If you have a Country entity with a lazy-loaded navigation property cities, EF will create a Proxy class that inherits from Country and overrides the cities property implementing there the lazy-loading logic.

Using the imperative mapping in SqlAlchemy gives you even more freedom. Your entities can have any constructor, as SqlAlchemy leverages Python's __new__ and __init__ separation so that it does not invoke __init__ for initializing the entities, but set attributes one by one. Then the dynamic nature of the language means that you don't have to mark in any special way properties corresponding to lazy loaded relationships and it does not need to resort to proxy classes to implement lazy loading, as it leverages Python dynamism and lookup logic. I think for each lazy relation in an entity a Descriptor is added to the class. When you first try to access the corresponding attribute the lookup will reach the Descriptor, that will perform the corresponding query and set the result in an attribute of the instance, so that the next time that you access the relation, the values will be retrieved from the instance. I guess this is more or less related to what I discuss here.

All this said, we should also note that (as explained here) there's still some "persistence leakage" into your entities when using the Imperative mapping. While you define your entity classes fully unaware of the persistence, SqlAlchemy (when adding them to the registry) makes them aware of the persistence mechanism by adding different attributes at the class level and at the instance level. For example attributes like _sa_instance_state or _sa_lazy_loader (these are part of SQLAlchemy’s internal machinery to track state and identity, manage lazy loading and relationship resolution and hook into attribute access dynamically). So your entities become bloated with extra attributes that you don't use on your own, and if you serialize them to json or whatever, they'll show up.

In the end I've ended up having separate Model entities (that use the declarative mapping) and Domain entities (that know nothing about the database) and mapper classes/functions that map Model entities to Domain entities and viceversa. This gives you almost full PI. I say almost cause you still end up with table ID's leaking into you Domain entities, but this is a more than acceptable compromise. Anyway, you still could get rid of it by declaring your Domain entities without the ID (Countr class) but declaring additional child entities (CountryIdAware class) that incorporate the ID. Your Model to Domain mappers will indeed create CountryIdAware instances that will be passed to your Domain, but the Domain will we aware of them just as User instances, it won't see the ID attribute.

No comments:

Post a Comment