Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why isn't SQLAlchemy default column value available before object is committed?

Recently I figured out that SQLAlchemy's Column default doesn't work as I expect it to:

>>> Base = declarative_base()
>>> class TestModel(Base):
    ...     __tablename__ = 'tmodel'
...     id = sa.Column(sa.Integer, primary_key=True)
...     foo = sa.Column(sa.Integer, default=0)
...    
>>> tmodel_instance = TestModel()
>>> print tmodel_instance.foo
None
>>> session.add(tmodel_instance)
>>> print tmodel_instance.foo
None
>>> session.commit()
>>> print tmodel_instance.foo
0

I want tmodel_instance.foo to equal 0 right after object instantiation, but it seems that the default value is only used when executing INSERT command, and it really confuses me. Why would one prefer the default over server_default? And how do I achieve what I want? Am I supposed to specify all default arguments in __init__? That seems to be code duplication: to change the default value I have to change it twice and maintain those values equality -- is there some way to avoid that?

like image 610
dir01 Avatar asked Dec 22 '12 11:12

dir01


Video Answer


3 Answers

one would prefer default over server default for one of four reasons:

  1. you'd like to run a Python function, not a SQL function, for the default (or a SQL expression that needs some per-INSERT Python state also).

  2. the default is part of a primary key column. the ORM can't load a row back without the primary key, so server_default is generally not useful for a PK column when using the ORM.

  3. the SQL expression you want to run isn't supported by the database as a "server default".

  4. You're dealing with a schema you can't/don't want to change.

In this case, when you'd like "foo" to be "0" in your application independent of database operations, the choices are:

  1. use __init__(). It's python!

  2. use events.

Here's __init__():

class TestModel(Base):
   # ...

   def __init__(self):
       self.foo = 0

Here's events (specifically the init event):

from sqlalchemy import event

@event.listens_for(Foo, "init")
def init(target, args, kwargs):
    target.foo = 0
like image 82
zzzeek Avatar answered Oct 11 '22 11:10

zzzeek


You can use force_instant_defaults listener from sqlalchemy_utils to change this behavior:

from sqlalchemy_utils import force_instant_defaults

force_instant_defaults()

class TestModel(Base):
    __tablename__ = 'tmodel'
    id = sa.Column(sa.Integer, primary_key=True)
    foo = sa.Column(sa.Integer, default=0)

model = TestModel()
assert model.foo == 0
like image 14
krassowski Avatar answered Oct 11 '22 11:10

krassowski


You can use the init event to fill defaults. This event listener will do it:

from sqlalchemy import event
from sqlalchemy.orm import mapper
from sqlalchemy.inspection import inspect


def instant_defaults_listener(target, args, kwargs):
    for key, column in inspect(target.__class__).columns.items():
        if column.default is not None:
            if callable(column.default.arg):
                setattr(target, key, column.default.arg(target))
            else:
                setattr(target, key, column.default.arg)


event.listen(mapper, 'init', instant_defaults_listener)
like image 7
Pedro Werneck Avatar answered Oct 11 '22 11:10

Pedro Werneck