On Sat, Mar 17, 2018 at 7:02 PM, Derek Lambert
<dlamb...@dereklambert.com> wrote:
> I'm probably overlooking something simple, looking for feedback before
> opening an issue.
>
> I have some objects with relationships defined between. When I create a new
> related object and pass it in the append() method of the collection
> everything works as expected, the foreign key is set. When I assign the new
> related object to a variable and pass that to the append() method, the
> foreign key isn't set and I get a 'null value in column "directory_name"
> violates not-null constraint'. This is with SQLAlchemy 1.2.5 and python 3.6.
>
> import sqlalchemy as sa
> import sqlalchemy.orm as orm
> from sqlalchemy.ext.declarative import declarative_base
>
>
> Base = declarative_base()
>
>
> class Directory(Base):
>     name = sa.Column(sa.String, primary_key=True)
>
>     __tablename__ = 'directory'
>
>
> class Category(Base):
>     name = sa.Column(sa.String, primary_key=True)
>
>     __tablename__ = 'category'
>
>
> class Filter(Base):
>     directory_name = sa.Column(sa.String, sa.ForeignKey('directory.name'),
> primary_key=True)
>     category_name  = sa.Column(sa.String, sa.ForeignKey('category.name'),
> primary_key=True)
>     filter         = sa.Column(sa.String, primary_key=True)
>
>     directory = orm.relationship('Directory', backref=orm.backref('filters',
> lazy='joined'), lazy='joined')
>     category  = orm.relationship('Category', backref=orm.backref('filters',
> lazy='joined'), lazy='joined')
>
>     __tablename__ = 'filter'
>
>
> engine =
> sa.create_engine('postgresql+psycopg2://postgres@localhost/bug_test')
> Base.metadata.create_all(engine)
> session = orm.sessionmaker(bind=engine)()
>
> directory = Directory(name='test')
> category_a = Category(name='category a')
> category_b = Category(name='category b')
>
> session.add(directory)
> session.add(category_a)
> session.add(category_b)
> session.commit()
>
> assert len(session.new) == 0
>
> # Instantiate object in call to append - works
> directory.filters.append(Filter(filter='test filter', category=category_a))
> session.commit()
>
> assert len(session.new) == 0
>
> # Instantiate object before call to append - fails
> new_filter = Filter(filter='new filter', category=category_b)
> directory.filters.append(new_filter)
>
> session.commit()
>
> assert len(session.new) == 0
>
>
> Thanks,
> Derek

The error I get (after swapping sqlite for postgresql) is:

sqlalchemy.exc.IntegrityError: (raised as a result of Query-invoked
autoflush; consider using a session.no_autoflush block if this flush
is occurring prematurely) (sqlite3.IntegrityError) NOT NULL constraint
failed: filter.directory_name [SQL: u'INSERT INTO filter
(category_name
, filter) VALUES (?, ?)'] [parameters: (u'category b', 'new filter')]

The reason this is happening is that accessing the "directory.filters"
attribute is causing an autoflush. new_filter is already in the
session at this point, so it gets flushed, but because it hasn't yet
been attached to the Directory, the directory_name attribute is still
None.

It's clear why this is happening in the second case. I wasn't sure why
it *wasn't* happening in the first instance, so I looked at the
bytecode:

 50           0 LOAD_GLOBAL              0 (directory)
              3 LOAD_ATTR                1 (filters)
              6 LOAD_ATTR                2 (append)
              9 LOAD_GLOBAL              3 (Filter)
             12 LOAD_CONST               1 ('filter')
             15 LOAD_CONST               2 ('test filter')
             18 LOAD_CONST               3 ('category')
             21 LOAD_GLOBAL              4 (category_a)
             24 CALL_FUNCTION          512
             27 CALL_FUNCTION            1
             30 POP_TOP

It looks like Python evaluates the "directory.filters.append"
expression before evaluating the parameters to the append() function.
So in the first instance, "directory.filters" is loaded *before* the
Filter is constructed, so it never gets flushed in a
partially-constructed state. I *think* this is guaranteed by the
language (https://docs.python.org/2/reference/expressions.html#evaluation-order)

As the error message says, you can work around this by disabling
autoflush, perhaps like this:

with session.no_autoflush:
    directory.filters.append(new_filter)

Or, if you don't actually want to load directory.filters at this point
in your code, use the other end of the relationship:

new_filter.directory = directory

Hope that helps,

Simon

-- 
SQLAlchemy - 
The Python SQL Toolkit and Object Relational Mapper

http://www.sqlalchemy.org/

To post example code, please provide an MCVE: Minimal, Complete, and Verifiable 
Example.  See  http://stackoverflow.com/help/mcve for a full description.
--- 
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to sqlalchemy+unsubscr...@googlegroups.com.
To post to this group, send email to sqlalchemy@googlegroups.com.
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.

Reply via email to