Instance <xxx> has been deleted after commit(), but when and why? - python

Here's a really simple piece of code. After adding the "poll" instance to the DB and committing, I cannot later read it. SQLAlchemy fails with the error:
Instance '<PollStat at 0x7f9372ea72b0>' has been deleted, or its row is otherwise not present.
Weirdly, this does not happen if I replace the ts_start/ts_run primary key by an integer autoincrement one. Is it possible that DateTime columns are not suitable as primary key?
db = Session()
poll = models.PollStat(
ts_start=datetime.datetime.now(),
ts_run=ts_run,
polled_tools=0)
db.add(poll)
db.commit() # I want to commit here in case something fails later
print(poll.polled_tools) # this fails
PollStat in module models.py:
class PollStat(Base):
__tablename__ = 'poll_stat'
ts_run = Column(Integer, primary_key=True)
ts_start = Column(DateTime, primary_key=True)
elapsed_ms = Column(Integer, default=None)
polled_tools = Column(Integer, default=0)
But if I do this:
class PollStat(Base):
__tablename__ = 'poll_stat'
id = Column(Integer, primary_key=True)
ts_run = Column(Integer)
ts_start = Column(DateTime)
elapsed_ms = Column(Integer, default=None)
polled_tools = Column(Integer, default=0)
it works. Why?

For anyone that still has this problem, this error happened to me because I submitted a JSON object with an id of 0; I use the same form for adding and editing said object, so when editing this would normally have an ID number, but when creating the item, the id property needs to be deleted before inserting the item. Some databases don't accept an ID of 0. In the end the row is created but the ID of 0 no longer matches the current ID, hence why the error pops up.

Related

SQLAlchemy unique constrain by field

I have UniqueConstraint on field, but it wont allow me to add multiple entries (two is max!)
from sqlalchemy import Column, Integer, String, Boolean, UniqueConstraint
class Cart(SqlAlchemyBase):
__tablename__ = 'cart'
__table_args__ = (UniqueConstraint('is_latest'), {})
sid = Column(Integer, primary_key=True)
is_latest = Column(Boolean, index=True, nullable=False)
name = Column(String)
I would like to support more entries, so that one name can have two variants:
name=foo, is_latest=True
name=foo, is_latest=False
name=bar, is_latest=True
name=bar, is_latest=False
but then reject any subsequent attempt to write name=foo (or bar) and is_latest=True
What you are trying to achieve here is a type 2 slowly changing dimension, this is a topic that has been discussed extensively and I encourage you to look it up.
When I look at your table you seem to use sid as a surrogate key, but I fail to see what is the natural key and what will be updated as time goes.
Anyway, there are several ways to achieve SCD type 2 result without the need to worry about your check, but the the simplest in my mind is to keep on adding records with your natural key and when querying, select only the one with highest surrogate key (autoincrementing integer), no need for current uniqueness here as only the latest value is fetched.
There are examples for versioning rows in SQLAlchemy docs, but since website come and go, I'll put a simplified draft of the above approach here.
class VersionedItem(Versioned, Base):
id = Column(Integer, primary_key=True) # surrogate key
sku = Column(String, index=True) # natural key
price = Column(Integer) # the value that changes with time
#event.listens_for(Session, "before_flush")
def before_flush(session, flush_context, instances):
for instance in session.dirty:
if not (
isinstance(instance, VersionedItem)
and session.is_modified(instance)
and attributes.instance_state(instance).has_identity
):
continue
make_transient(instance) # remove db identity from instance
instance.id = None # remove surrogate key
session.add(instance) # insert instance as new record
Looks like a Partial Unique Index can be used:
class Cart(SqlAlchemyBase):
__tablename__ = 'cart'
id = Column(Integer, primary_key=True)
cart_id = Column(Integer)
is_latest = Column(Boolean, default=False)
name = Column(String)
__table_args__ = (
Index('only_one_latest_cart', name, is_latest,
unique=True,
postgresql_where=(is_latest)),
)
name=foo, is_latest = True
name=foo, is_latest = False
name=bar, is_latest = False
name=bar, is_latest = False
And when adding another name=foo, is_latest = True
psycopg2.errors.UniqueViolation: duplicate key value violates unique constraint "only_one_latest_cart"
DETAIL: Key (name, is_latest)=(foo, t) already exists.

SQLAlchemy upsert Function for MySQL

I have used the following documentation as a guide and tried to implement an upset mechanism for my Games table. I want to be able to dynamically update all columns of the selected table at a time (without having to specify each column individually). I have tried different approaches, but none have provided a proper SQL query which can be executed. What did I misunderstand respectively what are the errors in the code?
https://docs.sqlalchemy.org/en/12/dialects/mysql.html?highlight=on_duplicate_key_update#insert-on-duplicate-key-update-upsert
https://github.com/sqlalchemy/sqlalchemy/issues/4483
class Game(CustomBase, Base):
__tablename__ = 'games'
game_id = Column('id', Integer, primary_key=True)
date_time = Column(DateTime, nullable=True)
hall_id = Column(Integer, ForeignKey(SportPlace.id), nullable=False)
team_id_home = Column(Integer, ForeignKey(Team.team_id))
team_id_away = Column(Integer, ForeignKey(Team.team_id))
score_home = Column(Integer, nullable=True)
score_away = Column(Integer, nullable=True)
...
def put_games(games): # games is a/must be a list of type Game
insert_stmt = insert(Game).values(games)
#insert_stmt = insert(Game).values(id=Game.game_id, data=games)
on_upset_stmt = insert_stmt.on_duplicate_key_update(**games)
print(on_upset_stmt)
...
I regularly load original data from an external API (incl. ID) and want to update my database with it, i.e. update the existing entries (with the same ID) with the new data and add missing ones without completely reloading the database.
Updates
The actual code results in
TypeError: on_duplicate_key_update() argument after ** must be a
mapping, not list
With the commented line #insert_statement... instead of first insert_stmt is the error message
sqlalchemy.exc.CompileError: Unconsumed column names: data

How to set the default value of a column in sqlalchemy to the value of a column from a relationship?

I am setting up a Sqlalchemy mapper for a sqlite database. My User class has a non-nullable relationship with my Team class. The code I already have is as follows:
class Team(Base):
__tablename__ = 'teams'
team_id = Column(Integer, primary_key=True)
# Using Integer as holder for boolean
is_local = Column(Integer, default=0)
class User(Base):
__tablename__ = 'users'
user_id = Column(Integer, primary_key=True)
team_id = Column(Integer, ForeignKey(Team.team_id), default=1, nullable=False)
team = relationship('Team')
is_local = Column(Integer, default=0)
I would like to establish that the value of User.is_local is by default the value of Team.is_local for the User's linked team.
However, after the creation of the User, I would still like the ability to modify the user's is_local value without changing the values of the team or any other user on the team.
So if I were to execute
faraway = Team(is_local=1)
session.add(faraway)
session.commit()
u = User(team=faraway)
session.add(u)
session.commit()
print(bool(u.is_local))
The result should be True
So far, I have tried context-sensitive default functions as suggested by https://stackoverflow.com/a/36579924, but I have not been able to find the syntax allowing me to reference Team.is_local
Is there a simple way to do this?
The first suggestion from SuperShoot, using a sql expression as the default appears to work. Specifically,
is_local = Column(Integer, default=select([Team.is_local]).where(Team.team_id==team_id))
gives me the logic I require.

SQLAlchemy: list of objects, preserve reference if they're deleted

I'm trying to implement a user-facing PreviewList of Articles, which will keep its size even if an Article is deleted. So if the list has four objects [1, 2, 3, 4] and one is deleted, I want it to contain [1, 2, None, 4].
I'm using a relationship with a secondary table. Currently, deleting either Article or PreviewList will delete the row in that table. I've experimented with cascade options, but they seem to affect the related items directly, not the contents of the secondary table.
The snippet below tests for the desired behaviour: deleting an Article should preserve the row in ArticlePreviewListAssociation, but deleting a PreviewList should delete it (and not the Article).
In the code below, deleting the Article will preserve the ArticlePreviewListAssociation, but pl.articles does not treat that as a list entry.
from db import DbSession, Base, init_db
from sqlalchemy import Column, String, Integer, ForeignKey
from sqlalchemy.orm import relationship
session = DbSession()
class Article(Base):
__tablename__ = 'articles'
id = Column(Integer, primary_key=True)
title = Column(String)
class PreviewList(Base):
__tablename__ = 'preview_lists'
id = Column(Integer, primary_key=True)
articles = relationship('Article', secondary='associations')
class ArticlePreviewListAssociation(Base):
__tablename__ = 'associations'
article_id = Column(Integer, ForeignKey('articles.id'), nullable=True)
previewlist_id = Column(Integer, ForeignKey('preview_lists.id'), primary_key=True)
article = relationship('Article')
preview_list = relationship('PreviewList')
init_db()
print(f"Creating test data")
a = Article(title="StackOverflow: 'Foo' not setting 'Bar'?")
pl = PreviewList(articles=[a])
session.add(a)
session.add(pl)
session.commit()
print(f"ArticlePreviewListAssociations: {session.query(ArticlePreviewListAssociation).all()}")
print(f"Deleting PreviewList")
session.delete(pl)
associations = session.query(ArticlePreviewListAssociation).all()
print(f"ArticlePreviewListAssociations: should be empty: {associations}")
if len(associations) > 0:
print("FAIL")
print("Reverting transaction")
session.rollback()
print("Deleting article")
session.delete(a)
articles_in_list = pl.articles
associations = session.query(ArticlePreviewListAssociation).all()
print(f"ArticlePreviewListAssociations: should not be empty: {associations}")
if len(associations) == 0:
print("FAIL")
print(f"Articles in PreviewList: should not be empty: {articles_in_list}")
if len(articles_in_list) == 0:
print("FAIL")
# desired outcome: pl.articles should be [None], not []
print("Reverting transaction")
session.rollback()
This may come down to "How can you make a many-to-many relationship where pk_A == 1 and pk_B == NULL include the None in A's list?"
The given examples would seem to assume that the order of related articles is preserved, even upon deletion. There are multiple approaches to that, for example the Ordering List extension, but it is easier to first solve the problem of preserving associations to deleted articles. This seems like a use case for an association object and proxy.
The Article class gets a new relationship so that deletions cascade in a session. The default ORM-level cascading behavior is to set the foreign key to NULL, but if the related association object is not loaded, we want to let the DB do it, so passive_deletes=True is used:
class Article(Base):
__tablename__ = 'articles'
id = Column(Integer, primary_key=True)
title = Column(String)
previewlist_associations = relationship(
'ArticlePreviewListAssociation', back_populates='article',
passive_deletes=True)
Instead of a many to many relationship PreviewList uses the association object pattern, along with an association proxy that replaces the many to many relationship. This time the cascades are a bit different, since the association object should be deleted, if the parent PreviewList is deleted:
class PreviewList(Base):
__tablename__ = 'preview_lists'
id = Column(Integer, primary_key=True)
article_associations = relationship(
'ArticlePreviewListAssociation', back_populates='preview_list',
cascade='all, delete-orphan', passive_deletes=True)
articles = association_proxy(
'article_associations', 'article',
creator=lambda a: ArticlePreviewListAssociation(article=a))
Originally the association object used previewlist_id as the primary key, but then a PreviewList could contain a single Article only. A surrogate key solves that. The foreign key configurations include the DB level cascades. These are the reason for using passive deletes:
class ArticlePreviewListAssociation(Base):
__tablename__ = 'associations'
id = Column(Integer, primary_key=True)
article_id = Column(
Integer, ForeignKey('articles.id', ondelete='SET NULL'))
previewlist_id = Column(
Integer, ForeignKey('preview_lists.id', ondelete='CASCADE'),
nullable=False)
# Using a unique constraint on a nullable column is a bit ugly, but
# at least this prevents inserting an Article multiple times to a
# PreviewList.
__table_args__ = (UniqueConstraint(article_id, previewlist_id), )
article = relationship(
'Article', back_populates='previewlist_associations')
preview_list = relationship(
'PreviewList', back_populates='article_associations')
With these changes in place no "FAIL" is printed.

sqlalchemy cascade and association objects

My database structure is something like this (I'm using declarative style):
class Character(Base):
__tablename__="characters"
id = Column(Integer, primary_key=True)
name = Column(String)
player = Column(String)
inventory = relation(Inventory)
class Item(Base):
__tablename__="items"
id = Column(Integer, primary_key=True)
name = Column(String)
class Inventory(Base):
__tablename__="inventory"
id = Column(Integer, primary_key=True)
char_id = Column(Integer, ForeignKey("characters.id"))
item_id = Column(Integer, ForeignKey("characters.id"))
quantity = Column(Integer)
item = relation(Item)
My problem is that when I remove an "Inventory" object from "Character.inventory" this isn't updated until the session get committed. For example:
>>> torch_inv=character.inventory[0] # Inventory object referred to a torch
>>> torch_inv.item, torch_inv.quantity
(<Item object, torch>, 3)
>>> session.delete(torch_inv)
>>> character.inventory[0]
<Inventory object, torch>
I've seen that there is a relation option "cascade" but I can't find a way to make it working in this case.
Session.delete() method just marks an instance as "to be deleted", so your relation won't change untill you flush changes to database independent on cascade rules. On other hand you can just remove Inventory instance from character.inventory collection, then having 'delete-orphan' cascade rule will mark removed Inventory instance for deletion.

Categories