Issue: Setting of models onto foreign key fields yields NULL value
Author: conqp
State: closed
Created: 2018-02-02 08:44:21

I have a strange problem with the new peewee-3.0.
When I set a model instance onto a `ForeignKeyField` descriptor of another model instance and try to save the latter model, I get the an error.

Debug code in question (excerpt):

    for model in record:
        try:
            model.save()
        except:
            print('Data:', model.__data__)
            print('Rel:', model.__rel__)
            print('Model.immobilie:', model.immobilie)
            print('Model.immobilie.id:', model.immobilie.id)
            print(format_exc(), flush=True)
            raise

Output:

    Data: {'immobilie': None, 'klasse': 'wohnung', 'typ': 'DACHGESCHOSS'}
    Rel: {'immobilie': <openimmodb.Immobilie object at 0x7f9d63fd8710>}
    Model.immobilie: <openimmodb.Immobilie object at 0x7f9d63fd8710>
    Model.immobilie.id: 276731
    Traceback (most recent call last):
    File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 2475, in execute_sql
     cursor.execute(sql, params or ())
    File "/usr/local/lib/python3.4/dist-packages/pymysql/cursors.py", line 165, in execute
     result = self._query(query)
    File "/usr/local/lib/python3.4/dist-packages/pymysql/cursors.py", line 321, in _query
     conn.query(q)
    File "/usr/local/lib/python3.4/dist-packages/pymysql/connections.py", line 860, in query
     self._affected_rows = self._read_query_result(unbuffered=unbuffered)
    File "/usr/local/lib/python3.4/dist-packages/pymysql/connections.py", line 1061, in _read_query_result
     result.read()
    File "/usr/local/lib/python3.4/dist-packages/pymysql/connections.py", line 1349, in read
     first_packet = self.connection._read_packet()
    File "/usr/local/lib/python3.4/dist-packages/pymysql/connections.py", line 1018, in _read_packet
     packet.check_error()
    File "/usr/local/lib/python3.4/dist-packages/pymysql/connections.py", line 384, in check_error
     err.raise_mysql_exception(self._data)
    File "/usr/local/lib/python3.4/dist-packages/pymysql/err.py", line 107, in raise_mysql_exception
     raise errorclass(errno, errval)
    pymysql.err.IntegrityError: (1048, "Column 'immobilie' cannot be null")
    During handling of the above exception, another exception occurred:
    Traceback (most recent call last):
    File "/usr/local/lib/python3.4/dist-packages/homie/mods/oidb/interface.py", line 74, in update
     model.save()
    File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 5154, in save
     pk_from_cursor = self.insert(**field_dict).execute()
    File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 1442, in inner
     return method(self, database, *args, **kwargs)
    File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 1513, in execute
     return self._execute(database)
    File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 2142, in _execute
     return super(Insert, self)._execute(database)
    File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 1920, in _execute
     cursor = database.execute(self)
    File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 2488, in execute
     return self.execute_sql(sql, params, commit=commit)
    File "/usr/local/lib/python3.4/dist-packages/peeweeplus/database.py", line 22, in execute_sql
     return super().execute_sql(*args, **kwargs)
    File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 2482, in execute_sql
     self.commit()
    File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 2278, in __exit__
     reraise(new_type, new_type(*exc_args), traceback)
    File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 169, in reraise
     raise value.with_traceback(tb)
    File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 2475, in execute_sql
     cursor.execute(sql, params or ())
    File "/usr/local/lib/python3.4/dist-packages/pymysql/cursors.py", line 165, in execute
     result = self._query(query)
    File "/usr/local/lib/python3.4/dist-packages/pymysql/cursors.py", line 321, in _query
     conn.query(q)
    File "/usr/local/lib/python3.4/dist-packages/pymysql/connections.py", line 860, in query
     self._affected_rows = self._read_query_result(unbuffered=unbuffered)
    File "/usr/local/lib/python3.4/dist-packages/pymysql/connections.py", line 1061, in _read_query_result
     result.read()
    File "/usr/local/lib/python3.4/dist-packages/pymysql/connections.py", line 1349, in read
     first_packet = self.connection._read_packet()
    File "/usr/local/lib/python3.4/dist-packages/pymysql/connections.py", line 1018, in _read_packet
     packet.check_error()
    File "/usr/local/lib/python3.4/dist-packages/pymysql/connections.py", line 384, in check_error
     err.raise_mysql_exception(self._data)
    File "/usr/local/lib/python3.4/dist-packages/pymysql/err.py", line 107, in raise_mysql_exception
     raise errorclass(errno, errval)
    peewee.IntegrityError: (1048, "Column 'immobilie' cannot be null")

What is the problem here?

Okay, I think I understand the error now.

Assuming:

    class Parent(Model):
        pass

    class Child(Model):
        mom = ForeignKeyField(Parent)
        dad = ForeignKeyField(Parent)

In peewee < 3.0 it was possible to do this:

    mom = Parent()
    dad = Parent()
    daughter = Child()
    daughter.mom = mom  # (1)
    daughter.dad = dad  # (2)

    for model in (mom, dad, daughter):
        model.save()

This will not work in peewee 3.0 any longer, since in `(1)` and `(2)` the respective `ForeignKeyAccessor` will immediately add the not yet set primary key of `mom` resp. `dad` to `daughter.__data__['mom']` resp. `daughter.__data__['dad']` as per https://github.com/coleifer/peewee/blob/406eb5865d72f1c74f48abd67bb8f4c9ccc1391a/peewee.py#L3490

Can you fix this to allow deferring of saving the models (and thus assigning foreign keys)?

Maybe you could check for `instance.__rel__[self.name]._pk` if and only if `instance.__data__[self.name]` is None on `Model.save()`?

Because this totally breaks my workflow. :(

Thanks for the patch. Added a test to help prevent regressions.