dimanche 13 janvier 2019

How can I pass a nameko dependency to an SqlAlchemy event handler?

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