Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

sqlalchemy: how to block updates on a specific column

Tags:

sqlalchemy

I have a declarative mapping:

class User(base):
    username = Column(Unicode(30), unique=True)

How can I tell sqlalchemy that this attribute may not be modified? The workaround I came up with is kindof hacky:

from werkzeug.utils import cached_property
# regular @property works, too

class User(base):
    _username = Column('username', Unicode(30), unique=True)
    @cached_property
    def username(self):
        return self._username
    def __init__(self, username, **kw):
        super(User,self).__init__(**kw)
        self._username=username

Doing this on the database column permission level will not work because not all databases support that.

like image 423
Matthias Urlichs Avatar asked May 04 '14 11:05

Matthias Urlichs


2 Answers

You can use the validates SQLAlchemy feature.

from sqlalchemy.orm import validates
...

class User(base):
  ...
  
  @validates('username')
  def validates_username(self, key, value):
    if self.username:  # Field already exists
      raise ValueError('Username cannot be modified.')

    return value

reference: https://docs.sqlalchemy.org/en/13/orm/mapped_attributes.html#simple-validators

like image 148
AlexQueue Avatar answered Sep 19 '22 04:09

AlexQueue


I can suggest the following ways do protect column from modification:

First is using hook when any attribute is being set:

In case above all column in all tables of Base declarative will be hooked, so you need somehow to store information about whether column can be modified or not. For example you could inherit sqlalchemy.Column class to add some attribute to it and then check attribute in the hook.

class Column(sqlalchemy.Column):

    def __init__(self, *args, **kwargs):
        self.readonly = kwargs.pop("readonly", False)
        super(Column, self).__init__(*args, **kwargs)

# noinspection PyUnusedLocal
@event.listens_for(Base, 'attribute_instrument')
def configure_listener(class_, key, inst):
    """This event is called whenever an attribute on a class is instrumented"""

    if not hasattr(inst.property, 'columns'):
        return

    # noinspection PyUnusedLocal
    @event.listens_for(inst, "set", retval=True)
    def set_column_value(instance, value, oldvalue, initiator):
        """This event is called whenever a "set" occurs on that instrumented attribute"""
        logging.info("%s: %s -> %s" % (inst.property.columns[0], oldvalue, value))
        column = inst.property.columns[0]

        # CHECK HERE ON CAN COLUMN BE MODIFIED IF NO RAISE ERROR
        if not column.readonly:
            raise RuntimeError("Column %s can't be changed!" % column.name)

        return value

To hook concrete attributes you can do the next way (adding attribute to column not required):

# standard decorator style
@event.listens_for(SomeClass.some_attribute, 'set')
def receive_set(target, value, oldvalue, initiator):
    "listen for the 'set' event"
    # ... (event handling logic) ...

Here is guide about SQLAlchemy events.

Second way that I can suggest is using standard Python property or SQLAlchemy hybrid_property as you have shown in your question, but using this approach result in code growing.

P.S. I suppose that compact way is add attribute to column and hook all set event.

like image 20
mblw Avatar answered Sep 20 '22 04:09

mblw