Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Apply a global filter to all tables for every query in SQLAlchemy

Tags:

sqlalchemy

We are trying to setup a SaaS service that supports multi-tenancy in a shared database and schema. What we are planning is to have a tenant_id column on all our tables. what I would like to do, is without the developer having to write any extra code is for my queries to automatically filter all involved tables by this tenant id. Is there a transparent way to achieve this in SQL Alchemy?

I found how you can override the default query object:

self.session = sessionmaker(bind=engine, query_cls=TenantLimitingQuery)

But inside that TenantLimitingQuery how can apply it to all involved tables?

class TenantLimitingQuery(Query):
    def get(self, ident):
        #apply filter here

My tables have the same column to identify the tenant called tenant_id so in that get function i need to filter by tenant_id=current_tenant_id

like image 899
valanto Avatar asked Oct 04 '16 12:10

valanto


People also ask

What does SQLAlchemy all () return?

As the documentation says, all() returns the result of the query as a list.

What does query all () return?

all() will return all records which match our query as a list of objects.

What is _sa_instance_state in SQLAlchemy?

_sa_instance_state is a non-database-persisted value used by SQLAlchemy internally (it refers to the InstanceState for the instance. While not directly relevant to this section, if we want to get at it, we should use the inspect() function to access it).

What is lazy SQLAlchemy?

Lazy loading refers to objects are returned from a query without the related objects loaded at first. When the given collection or reference is first accessed on a particular object, an additional SELECT statement is emitted such that the requested collection is loaded.


1 Answers

This is outlined in the usage recipes wiki, reproduced here:

from sqlalchemy.orm.query import Query

class LimitingQuery(Query):

    def get(self, ident):
        # override get() so that the flag is always checked in the 
        # DB as opposed to pulling from the identity map. - this is optional.
        return Query.get(self.populate_existing(), ident)

    def __iter__(self):
        return Query.__iter__(self.private())

    def from_self(self, *ent):
        # override from_self() to automatically apply
        # the criterion too.   this works with count() and
        # others.
        return Query.from_self(self.private(), *ent)

    def private(self):
        mzero = self._mapper_zero()
        if mzero is not None:
            crit = mzero.class_.public == True

            return self.enable_assertions(False).filter(crit)
        else:
            return self

The idea is to apply the filter on demand, when the query object is iterated through.

If you want the filter to be applied to relationships as well, you'll need to use this recipe instead.

like image 187
univerio Avatar answered Sep 22 '22 14:09

univerio