Search code examples
pythonsqlalchemypyramid

Cannot properly update SQLAlchemy row


I am working with sqlalchemy scoped session and can't update an existing row properly.

Here are my models:

db = scoped_session(sessionmaker())
Base = declarative_base()

class Song(Base):
    __tablename__ = 'song'
    id = Column(Integer, primary_key=True)
    artist_id = Column(Integer, ForeignKey('artist.id'))
    artist_title = Column(Text)
    title = Column(Text)
    artist = relationship('Artist', backref='songs')
    preview_url = Column(Text, default=None)


class Artist(Base):
    __tablename__ = 'artist'
    id = Column(Integer, primary_key=True)
    title = Column(Text)

    similar_artists = relationship('Artist',
                                   secondary=followers,
                                   primaryjoin=(followers.c.follower_id == id),
                                   secondaryjoin=(followers.c.followed_id == id),
                                   backref=backref('followers', lazy='dynamic'),
                                   lazy='dynamic')

Index('my_index', Artist.id, unique=True, mysql_length=255)

I have populated the db and can see that Song rows have all the columns filled with data in the debugger and also pgweb interface. In the views I make a query to get a list of songs and want to update them as follows:

# song.artist_id has an integer value
song.preview_url = preview_url # Just a string
# db.query(Song).filter(Song.id == song.id).update({'preview_url':preview_url}) #Produces same result as above

db.commit()
# song.artist_id becomes None

This adds the preview_url to a row, however, once I do that and commit the artist.id becomes None in the song instance even though I was updating completely different field. I can observe that in the debugger and pgweb interface.

UPDATE: I have tried db.commit() right before I do any changes to the row and it still replaces song.artist_id with None. This implies that the row update has nothing to do with that, hence it must be preprocessing which I do on song. Is there any way I can get rid of all changes in the session before updating and committing the row?

Has anyone encountered this behavior? Do I have to explicitly set the artist.id again because it's a foreign key?


Solution

  • I managed to track down this fishy behavior. The problem was indeed with the preprocessing.

    Shortly, I was aggregating Artist.songs from many artists in a list and later on I was popping from this list, however what I forgot is that this list is special, i.e. it's an Instrumented Collection in SQLAlchemy, which according to the docs:

    "Instrumentation means that normal operations on the collection are tracked and result in changes being written to the database at flush time. "

    by popping from this collection I was actually deleting Artist-Song relations, therefore the foreign key was becoming None.

    My sample code looked something like that:

    artists = db.query(Artist).all()
    all_songs = []
    for artist in artists:
        all_songs.append(artist.songs)
    
    all_songs.pop(0) # This would delete the Artist-Song relation after the commit