开发者

Proxy pattern idiom

I'm a web application developer and in using SQLAlchemy I find it clumsy to do this in many of my controllers when I'm wanting a specific row from (say) the users table:

from model import dbsession # SQLAlchemy SessionMaker instance
from model import User

user = dbsession().query(User).filter_by(some_kw_args).first()

Or say I want to add a user to the table (assuming another controller):

from model import dbsession # SQLAlchemy SessionMaker instance
from model import User

user = User开发者_如何学运维("someval", "anotherval", "yanv")
dbsession().add(user)

So, because of that clumsiness (I won't go into some of my other personal idioms) I didn't like having to do all of that just to add a record to the table or to get a record from the table. So I decided (after a lot of nasty hacking on SQLAlchemy and deciding I was doing too many "magical" things) this was appropriate for the proxy pattern.

I (at first) did something like this inside of the model module:

def proxy_user(delete=False, *args, **kwargs):
    session = DBSession()

    # Keyword args? Let's instantiate it...
    if (len(kwargs) > 0) and delete:
        obj = session.query(User).filter_by(**kwargs).first()
        session.delete(obj)

        return True
    elif len(kwargs) > 0:
        kwargs.update({'removed' : False})
        return session.query(User).filter_by(**kwargs).first()
    else:
        # Otherwise, let's create an empty one and add it to the session...
        obj = User()
        session.add(obj)
        return obj

I did this for all of my models (nasty duplication of code, I know) and it works quite well. I can pass in keyword arguments to the proxy function and it handles all of the session querying for me (even providing a default filter keyword for the removed flag). I can initialize an empty model object and then add data to it by updating the object attributes and all of those changes are tracked (and committed/flushed) because the object has been added to the SQLAlchemy session.

So, to reduce duplication, I put the majority of the logic an decorator function and am now doing this:

def proxy_model(proxy):
    """Decorator for the proxy_model pattern."""

    def wrapper(delete=False, *args, **kwargs):

        model   = proxy()

        session = DBSession()

        # Keyword args? Let's instantiate it...
        if (len(kwargs) > 0) and delete:
            obj = session.query(model).filter_by(**kwargs).first()
            session.delete(obj)

            return True
        elif len(kwargs) > 0:
            kwargs.update({'removed' : False})
            return session.query(model).filter_by(**kwargs).first()
        else:
            # Otherwise, let's create an empty one and add it to the session...
            obj = model()
            session.add(obj)
            return obj

    return wrapper

# The proxy_model decorator is then used like so:
@proxy_model
def proxy_user(): return User

So now, in my controllers I can do this:

from model import proxy_user

# Fetch a user
user = proxy_user(email="someemail@ex.net") # Returns a user model filtered by that email

# Creating a new user, ZopeTransaction will handle the commit so I don't do it manually
new_user          = proxy_user()
new_user.email    = 'anotheremail@ex.net'
new_user.password = 'just an example'

If I need to do other more complex queries I will usually write function that handles it if I use it often. If it is a one-time thing I will just import the dbsession instance and then do the "standard" SQLAlchemy orm query....

This is much cleaner and works wonderfully but I still feel like it isn't "locked in" quite. Can anyone else (or more experienced python programmers) provide a better idiom that would achieve a similar amount of lucidity that I'm seeking while being a clearer abstraction?


You mention "didn't like having to do 'all of that'" where 'all of that' looks an awful lot like only 1 - 2 lines of code so I'm feeling that this isn't really necessary. Basically I don't really think that either statement you started with is all that verbose or confusing.

However, If I had to come up with a way to express this I wouldn't use a decorator here as you aren't really decorating anything. The function "proxy_user" really doesn't do anything without the decorator applied imo. Since you need to provide the name of the model somehow I think you're better of just using a function and passing the model class to it. I also think that rolling the delete functionality into your proxy is out of place and depending on how you've configured your Session the repeated calls to DBSession() may be creating new unrelated sessions which is going to cause problems if you need to work with multiple objects in the same transaction.

Anyway, here's a quick stab at how I would refactor your decorator into a pair of functions:

def find_or_add(model, session, **kwargs):
    if len(kwargs) > 0:
         obj = session.query(model).filter_by(**kwargs).first()
         if not obj:
             obj = model(**kwargs)
             session.add(obj)
    else:
         # Otherwise, let's create an empty one and add it to the session...
         obj = model()
         session.add(obj)
    return obj

def find_and_delete(model, session, **kwargs):
    deleted = False
    obj = session.query(model).filter_by(**kwargs).first()
    if obj:
        session.delete(obj)
        deleted = True
    return deleted

Again, I'm not convinced this is necessary but I think I can agree that:

user = find_or_add(User, mysession, email="bob@localhost.com")

Is perhaps nicer looking than the straight SQLAlchemy code necessary to find / create a user and add them to session.

I like the above functions better than your current decorator approach because:

  • The names clearly denote what your intent is here, where I feel proxy_user doesn't really make it clear that you want a user object if it exists otherwise you want to create it.
  • The session is managed explicitly
  • They don't require me to wrap every model in a decorator
  • The find_or_add function always returns an instance of model instead of sometimes returning True, a query result set, or a model instance.
  • the find_and_delete function always returns a boolean indicated whether or not it was successfully able to find and delete the record specified in kwargs.

Of course you might consider using a class decorator to add these functions as methods on your model classes, or perhaps deriving your models from a base class that includes this functionality so that you can do something like:

# let's add a classmethod to User or its base class:
class User(...):
    ...
    @classmethod
    def find_or_add(cls, session, **kwargs):
        if len(kwargs) > 0:
            obj = session.query(cls).filter_by(**kwargs).first()
            if not obj:
                obj = cls(**kwargs)
                session.add(obj)
        else:
            # Otherwise, let's create an empty one and add it to the session...
            obj = cls()
            session.add(obj)
        return obj
    ...
user = User.find_or_add(session, email="someone@tld.com")
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜