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?
one would prefer default over server default for one of four reasons:
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).
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.
the SQL expression you want to run isn't supported by the database as a "server default".
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:
use __init__()
. It's python!
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
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
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With