I'm writing a small RPC service that allows remote CRUD of a database, using nameko and sqlalchemy. For some methods/properties/event handlers, my models require to fetch some data using a dependency. The way I'd like it to work, whenever I call one of these methods for the first time during the lifetime of the model instance, the data is fetched and cached on the model. After that, the methods that need the external data will just use the cached version.
I'm having some trouble designing this. Passing the dependency as a function argument only works with model methods. Properties don't allow passing arguments and I don't control how SQL Alchemy's event handlers are called, so I can't inject any dependency there. The only way I found to make this work is to bind the dependency to the model instance early in its lifetime, but I feel like this goes against the DI pattern.
model.py
from uuid import uuid4
import sqlalchemy as sa
from sqlalchemy import event
from sqlalchemy.dialects import postgresql
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
metadata = Base.metadata
class Foo(Base):
__tablename__ = 'foo'
id = sa.Column(postgresql.UUID(as_uuid=True), primary_key=True, default=uuid4)
remote_id = sa.Column(sa.Integer, nullable=False, unique=True)
bar = sa.Column(sa.String, nullable=True)
def __init__(self, *args, **kwargs):
super(Foo, self).__init__(*args, **kwargs)
self._remote_data = None
@property
def remote_service(self):
# somehow return dependency
pass
@property
def remote_data(self):
if self._remote_data is None:
self._remote_data = self.remote_service.get(id=self.remote_id)
return self._remote_data
@property
def baz(self):
return self.bar + self.remote_data.baz
def do_before_insert(mapper, connection, foo):
# do something depending on value in foo.remote_data
pass
event.listen(Foo, 'before_insert', do_before_insert)
service.py
from nameko.extensions import DependencyProvider
from nameko.rpc import rpc
from nameko_sqlalchemy import Database
from .model import Base, Foo
class RemoteDataService(object):
def get(self, remote_id):
pass
class RemoteDataServiceProvider(DependencyProvider):
def get_dependency(self, worker_ctx):
return RemoteDataService()
class FooRPC:
name = "foo_rpc"
db = Database(Base)
@rpc
def get_foo(self, foo_id):
with self.db.get_session() as session:
foo = session.query(Foo).get(foo_id)
return foo
@rpc
def create_foo(self, remote_id, bar=None):
with self.db.get_session() as session:
foo = Foo(remote_id=remote_id, bar=bar)
session.add(foo)
session.commit()
return foo
One of my solutions involves binding the RemoteDataService
instance returned by the dependency provider to a custom database session at session creation time, then having the dependency property on the model look something like that:
from sqlalchemy.orm import object_session
...
@property
def remote_service(self):
return object_session(self).get_remote_service()
That solves my problems but it doesn't seem very DI compliant. Also it only works for models that are already bound to a session, but in my specific case, I can live with that. That being said, I'll take a better solution if there is one.
Is what I'm trying to do inherently wrong in the DI/nameko/sqla realm? Should models never deal directly with dependencies? Either way how does one reconcile the use of SQLalchemy's event handlers (a typical case where you have little to no control over the function call), DI, and the need to use a dependency in said handler?
Interesting question. Not sure that my answer is what you need. But maybe this will be useful. In any case, you can just see a different approach (better than nothing).
Models and dependencies.
Not sure that models
is a good place to call some services or dependencies. I think is good place to store a tiny logic based on model structure. And that's it. Something like:
@property
def full_price(self):
return self.coefficient * self.price
@property
def full_address(self):
return ' '.join([self.country, self.city, self.street])
Events. before_insert
, before_update
etc.
The same as models. Is a good place to update/set some fields of model. Something like:
foo.counter += 1
or foo.updated_time = datetime.utcnow()
. But not good place to process some 'remote data'.
It is better to store all other logic (for example, saving / cache / retrieving data from any service, I mean self.remote_service.get (...)
etc) on another layer.
I try to explain what I mean using a small example.
Note! I didn't work with nameko and I don't know best practice etc. So this is just a vision.
See comments in repo. Hope this helps.