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.dependency.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
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.
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?
Aucun commentaire:
Enregistrer un commentaire