2016-09-08 4 views
1

Я пытаюсь использовать расширение автозапуска SQLAlchemy для создания ORM для существующей базы данных и получаю исключение InvalidRequestError («Экземпляр не может быть обновлен - он не является постоянным и не содержит полный первичный ключ. ") всякий раз, когда я пытаюсь вставить в таблицу, которая использует составной первичный ключ, состоящий из метки времени и внешнего ключа.SQLAlchemy InvalidRequestError при вставке в automap сгенерированный ORM

Вот некоторый минимальный пример кода, который воспроизводит проблему:

from sqlalchemy import create_engine, func, select 
from sqlalchemy.orm import sessionmaker 
from sqlalchemy.sql.expression import text 
from sqlalchemy.ext.automap import automap_base 

db_schema_cmds = [ 
    '''CREATE TABLE users 
    (
     u_id INTEGER NOT NULL, 
     name TEXT NOT NULL, 
     CONSTRAINT Key1 PRIMARY KEY (u_id) 
    );''', 
    '''CREATE TABLE posts 
    (
     timestamp TEXT NOT NULL, 
     text TEXT NOT NULL, 
     u_id INTEGER NOT NULL, 
     CONSTRAINT Key2 PRIMARY KEY (timestamp,u_id), 
     CONSTRAINT users_have_posts FOREIGN KEY (u_id) REFERENCES users (u_id) ON DELETE CASCADE 
    );'''] 

# Create a new in-memory SQLite DB and execute the schema SQL commands. 
db_engine = create_engine('sqlite://') 
with db_engine.connect() as db_conn: 
    for cmd in db_schema_cmds: 
     db_conn.execute(text(cmd)) 

# Use automap to reflect the DB schema and generate ORM classes. 
Base = automap_base() 
Base.prepare(db_engine, reflect=True) 

# Create aliases for the table classes generated. 
User = Base.classes.users 
Post = Base.classes.posts 

session_factory = sessionmaker() 
session_factory.configure(bind=db_engine) 

# Add a user and a post to the DB. 
session = session_factory() 
new_user = User(name="John") 
session.add(new_user) 
session.commit() 
new_post = Post(users=new_user, text='this is a test', timestamp=func.now()) 
session.add(new_post) 
session.commit() 

# Verify that the insertion worked. 
new_user_id = session.execute(select([User])).fetchone()['u_id'] 
new_post_fk_user_id = session.execute(select([Post])).fetchone()['u_id'] 
assert new_user_id == new_post_fk_user_id 

session.close() 

Запуск этого дает следующий отслеживающий:

Traceback (most recent call last): 
    File "reproduce_InvalidRequestError.py", line 67, in <module> 
    session.commit() 
    File "C:\Python\Python35\lib\site-packages\sqlalchemy\orm\session.py", line 801, in commit 
    self.transaction.commit() 
    File "C:\Python\Python35\lib\site-packages\sqlalchemy\orm\session.py", line 392, in commit 
    self._prepare_impl() 
    File "C:\Python\Python35\lib\site-packages\sqlalchemy\orm\session.py", line 372, in _prepare_impl 
    self.session.flush() 
    File "C:\Python\Python35\lib\site-packages\sqlalchemy\orm\session.py", line 2019, in flush 
    self._flush(objects) 
    File "C:\Python\Python35\lib\site-packages\sqlalchemy\orm\session.py", line 2137, in _flush 
    transaction.rollback(_capture_exception=True) 
    File "C:\Python\Python35\lib\site-packages\sqlalchemy\util\langhelpers.py", line 60, in __exit__ 
    compat.reraise(exc_type, exc_value, exc_tb) 
    File "C:\Python\Python35\lib\site-packages\sqlalchemy\util\compat.py", line 186, in reraise 
    raise value 
    File "C:\Python\Python35\lib\site-packages\sqlalchemy\orm\session.py", line 2107, in _flush 
    flush_context.finalize_flush_changes() 
    File "C:\Python\Python35\lib\site-packages\sqlalchemy\orm\unitofwork.py", line 395, in finalize_flush_changes 
    self.session._register_newly_persistent(other) 
    File "C:\Python\Python35\lib\site-packages\sqlalchemy\orm\session.py", line 1510, in _register_newly_persistent 
    instance_key = mapper._identity_key_from_state(state) 
    File "C:\Python\Python35\lib\site-packages\sqlalchemy\orm\mapper.py", line 2417, in _identity_key_from_state 
    for col in self.primary_key 
    File "C:\Python\Python35\lib\site-packages\sqlalchemy\orm\mapper.py", line 2417, in <listcomp> 
    for col in self.primary_key 
    File "C:\Python\Python35\lib\site-packages\sqlalchemy\orm\attributes.py", line 578, in get 
    value = state._load_expired(state, passive) 
    File "C:\Python\Python35\lib\site-packages\sqlalchemy\orm\state.py", line 474, in _load_expired 
    self.manager.deferred_scalar_loader(self, toload) 
    File "C:\Python\Python35\lib\site-packages\sqlalchemy\orm\loading.py", line 647, in load_scalar_attributes 
    "contain a full primary key." % state_str(state)) 
sqlalchemy.exc.InvalidRequestError: Instance <posts at 0x45d45f8> cannot be refreshed - it's not persistent and does not contain a full primary key. 

Если я добавить echo=True параметр в create_engine вызов, я вижу это генерируя следующий SQL для вставки. Этот SQL отлично работает, когда я запускаю его в DB Browser для SQLite.

INFO sqlalchemy.engine.base.Engine BEGIN (implicit) 
INFO sqlalchemy.engine.base.Engine SELECT users.u_id AS users_u_id, users.name AS users_name FROM users WHERE users.u_id = ? 
INFO sqlalchemy.engine.base.Engine (1,) 
INFO sqlalchemy.engine.base.Engine INSERT INTO posts (timestamp, text, u_id) VALUES (CURRENT_TIMESTAMP, ?, ?) 
INFO sqlalchemy.engine.base.Engine ('this is a test', 1) 
INFO sqlalchemy.engine.base.Engine ROLLBACK 

Я также попытался удалить параметр users из Post() и вместо того, чтобы добавить строку new_user.posts_collection.append(new_post) перед вызовом session.add(new_post), но это привело к тому же SQL генерируются и та же ошибка происходит.

Если я заменю составной ключ на новый целочисленный столбец PK, все будет работать нормально. (Не идеальное решение, хотя, как причина, я использую automap, чтобы отразить существующую БД, поэтому желательно не нужно изменять, что схемы децибелах.)

Я нашел подобный вопрос, SQLAlchemy InvalidRequestError when using composite foreign keys, однако что, похоже, связано с использованием наследования в классах ORM таблицы, и решение зависело от определения классов таблицы ORM, а не для отражения БД для их создания.

Редактировать: Я предположил, что эта проблема связана с тем, что мой составной первичный ключ содержал внешний ключ. Принятый ответ показывает, что внешний ключ не был в значительной степени фактором для решения этой проблемы.

+0

Благодарим Вас за предоставление идеальный [mcve] (HTTP: // stackoverflow.com/help/mcve)! –

ответ

1

Проблема на самом деле не является составной первичной ключю с внешним ключом, но func.now() передан как timestamp, который является частью первичного ключа. Поскольку значение не известно SQLAlchemy, поскольку оно генерируется во время вставки в базу данных, оно не может выполнить post-fetch; он понятия не имеет, что взять. Если рассматриваемая БД поддерживает RETURNING или аналогичную, вы сможете это сделать. См. Примечание к triggered columns, в котором описывается эта точная ситуация. Предварительное выполнение SQL для значений первичного ключа также рассматривается в Defaults/SQL Expressions.

Причина, по которой он работает с целым суррогатным первичным ключом, заключается в том, что SQLite имеет механизм для fetching the last inserted row id (целочисленный столбец первичного ключа), который SQLAlchemy может использовать.

Чтобы исправить это, вы можете использовать метку времени, сгенерированный в Python

In [8]: new_post = Post(users=new_user, text='this is a test', 
    ...:     timestamp=datetime.utcnow()) 
    ...: session.add(new_post) 
    ...: session.commit() 
    ...: 

Другим решением было бы переопределить timestamp колонку при отражении и обеспечить func.now() по умолчанию. Это вызовет предварительное выполнение func.now().

...: # Use automap to reflect the DB schema and generate ORM classes. 
    ...: Base = automap_base() 
    ...: 
    ...: # Override timestamp column before reflection 
    ...: class Post(Base): 
    ...:  __tablename__ = 'posts' 
    ...:  timestamp = Column(Text, nullable=False, primary_key=True, 
    ...:      default=func.now()) 
    ...: 
    ...: Base.prepare(db_engine, reflect=True) 
    ...: 
    ...: # Create aliases for the table classes generated. 
    ...: User = Base.classes.users 
    ...: # Post has already been declared 
    ...: #Post = Base.classes.posts 

С по умолчанию на месте, вы не должны (и не должны) обеспечить timestamp при создании новых экземпляров

In [6]: new_post = Post(users=new_user, text='this is a test') 
    ...: session.add(new_post) 
    ...: session.commit() 
    ...: 
+0

Фантастическое объяснение, спасибо! Я отредактирую свой пост, чтобы исключить предположение о внешних ключах. –

Смежные вопросы