SQLAlchemy session error while creating object with m2m relation - python

I'm new to SQLAlchemy and Flask. I'm trying to create an object (book) with m2m relation and append an existed object (tag_2) to the relation:
book = Book(title='title')
tag_1 = Tag(name='tag')
book.tags.append(tag_1) # New tag works well
tag_2 = Tag.query.get(123) # Get existed tag by id
print(tag_2) # >>> Tag #123
book.tags.append(tag_2) # ERROR: Object '<Tag at ...>' is already attached to session '1'
self.session.add(book)
self.session.commit()
Have no problems creating new related objects, but can't point the existing object.
My models:
book_tags = db.Table('book_tags', db.metadata,
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id')),
db.Column('book_id', db.Integer, db.ForeignKey('book.id'))
)
class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True)
class Book(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(300), nullable=False)
tags = db.relationship('Tag', secondary=book_tags)
I use sqlite as DB.

It seems that you use different self.session for getting Tag and for getting Book you are then trying to append Tag to. In order to be able to append object A to object B they must both exist in same session.
It's hard to tell how you create your session because your post only shows code that operates with it but make sure you have only one session.

tags = db.relationship(
'Tag',
secondary=book_tags,
backref=db.backref('book', lazy='dynamic')
)
You need to add backref in m2m relationship because
Using backref just automates the creation of a relationship property at the other end. backref='book' is somewhat akin to having book = db.relationship('book') explicitly in the Tag class (+ back population). Using the backref() object you can pass arguments to that relationship.

Related

SQLAlchemy one-to-many relationship - how to properly get the 'many' collection

I'm not sure I properly understand how to get the collection part of the one-to-many relationship.
class ProjectReport(db.Model):
__tablename__ = "project_reports"
id = db.Column(UUID, primary_key=True, default=uuid.uuid4)
project_id = db.Column(UUID, db.ForeignKey("projects.id"), nullable=False)
entries = db.relationship("ProducerEntry", backref="project_report", lazy="dynamic")
class ProducerEntry(Entry):
__tablename__ = "producer_entries"
__mapper_args__ = {"polymorphic_identity": "Entry"}
id = db.Column(UUID, db.ForeignKey("entries.id"), primary_key=True)
project_id = db.Column(UUID, db.ForeignKey("projects.id"), nullable=False)
project_report_id = db.Column(UUID, db.ForeignKey("project_reports.id"), nullable=True)
My problem is that I can't just access the entries field.
for entry in self.entries:
do_something(entry)
This returns NotImplementedError
I managed to get the data via hybrid property but that seems a bit of an overkill since already have the relationship, also it'd get a bit complex for further logic later on.
#hybrid_property
def entries(self):
return ProducerEntry.query.filter_by(project_report_id=self.id)
Ab additional information is that the ProjectReport is basically the common columns of the Entry and Project models, and the project_report_id is nullable, because the entries and projects are generated first and then I can generate the project reports from them. This is how I create the reports:
...
project_report = ProjectReport(date_order=entry.date_order, project_id=entry.project.id)
project_report.entries.append(entry)
...
As far as I know I don't have to add the project_report_id to the producer entry after this.
What am I missing here?
Well yeah, that relationship field returns a query, so I simply should have called:
self.entries.all()
Or anything else which is handling a query.

What happens if I specify OneToMany relationship only from one side in flask-sqlalchemy?

I have a OneToMany relationship between 2 entities in flask. I also specified the relationship only on one side. I am unsure what the difference is between the following:
class CustomJob(db.Model):
__tablename__ = "custom_job"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
country_from = db.Column(db.Integer, db.ForeignKey('country.id'))
class Country(db.Model):
__tablename__ = "country"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
custom_jobs = db.relationship('CustomJob', backref="country", lazy=False)
Or just specify the foreign key on master entity:
class CustomJob(db.Model):
__tablename__ = "custom_job"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
country_from = db.Column(db.Integer, db.ForeignKey('country.id'))
will is there performance difference between the two ?
The brilliance behind an ORM like SQLAlchemy is that it can detect relationships between models based on foreign key constraints. So once you've declared your foreign key on the custom_job table, the relationship is configured in the database.
Mapping that relationship to your python objects is another useful part of ORM's. Here, you are doing that with db.relationship. By specifying backref, you are essentially telling the ORM to make the relationship available on the other object.
Let me explain more explicitly using the code provided in your Q:
class Country(db.Model):
__tablename__ = 'country'
...
custom_jobs = db.relationship('CustomJob', backref='custom_job', lazy=False)
...
The Country model you've defined will map all associated rows from the custom_job table through the attribute Country.custom_jobs.
This relationship will propagate to the CustomJob model and allow you to access the associated rows from the country table through an attribute created by the backref parameter --> here CustomJob.custom_job.
I assume this is an error and that you intended to use backref="country"
In this case, access associated objects instead with CustomJob.country

What could be the cause of "Dependency rule tried to blank-out primary key column"

When im trying to delete category instance identified by 'id' with its category_image and files instances the way like this:
c = Category.query.get(id)
for ci in c.images:
db.session.delete(ci)
db.session.flush()
for ci in c.images:
db.session.delete(ci.file)
db.session.flush() # if i type here db.session.commit() all is fine
db.session.delete(c)
db.session.commit()
i'm getting a AssertionError: Dependency rule tried to blank-out primary key column 'category_image.id_category' on instance ''. But when i replace flush which is after deleting category_image.files with commit, then it works. I've notice it after i changed CategoryImage table to intermediary. Before changes it has it's own pk that wasn't combined and all was working properly. Here're my current models definitions.
class File(db.Model):
__tablename__ = 'file'
id_file = Column(Integer, Sequence('seq_id_file'), primary_key=True, nullable=False)
name = Column(Text, nullable=False)
path = Column(Text, nullable=False, unique=True)
protected = Column(Boolean, nullable=False, default=False)
class Category(db.Model):
__tablename__ = 'category'
id_category = Column(Integer, Sequence('seq_id_category'), primary_key=True, nullable=False)
name = Column(UnicodeText, nullable=False, unique=True)
images = relationship('CategoryImage', backref='images')
class CategoryImage(db.Model):
__tablename__ = 'category_image'
__table_args__ = (
PrimaryKeyConstraint('id_category', 'id_file', name='seq_id_category_image'),
)
id_category = Column(Integer, ForeignKey(Category.id_category), nullable=False)
id_file = Column(Integer, ForeignKey(File.id_file), nullable=False)
id_size_type = Column(Integer, nullable=)
file = relationship(File)
Now i'm trying to figure out what just happened. Correct me if i'm using things wrong.
I just noticed that i have to delete objects beeing in relation with intermediate model in the same order as it was declared in table_args, PrimaryKeyConstraint('id_category', 'id_file'). So when i perform it this way: session.delete(category_image), session.delete(category), session.delete(file) and commit it or flush everywhere before commit, then all works fine. If anyone spot something about it in alch docs let me know.
Here is what is happening. Once you call session.delete() on some object it is like having marked the object for deletion but not yet deleted from db. when you call the flush() after deleting (note: db still has the object as it is yet not committed) but session has marked the object as deleted. So the objects become inconsistent. In order to make the delete smooth you can always wrap your delete operations within a transaction and once they are deleted from the session you need to call the db.commit() once to make db session consistent with the db.
Hope it helps.

SQLAlchemy Object is already attached to session, then DetachedInstanceError on refresh, still adds to database

I'm having some trouble with making a Many-to-Many relationship in Flask using SQLAlchemy. I have my two models, and the relationship table. When I create a Group object, I want that group to have the user who creates the group as a member, and the user should have the group being created as a group.
However, when I submit the form to create the group, I get a InvalidRequestError, saying Object '<User at 0x7f85ad606a50>' is already attached to session '1' (this is '2')
Then, if I refresh the page, resubmitting the form, it successfully creates the group and database relationship. However, it shows a DetachedInstanceError, saying that Parent instance <User at 0x7f85ad606a50> is not bound to a Session; lazy load operation of attribute 'groups' cannot proceed. That error shows until I restart the server.
Relevant code:
db:
db = SQLAlchemy(app)
Models:
groups = db.Table('user_groups',
db.Column('group_id', db.Integer, db.ForeignKey('groups.id')),
db.Column('user_id', db.Integer, db.ForeignKey('users.id'))
)
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
groups = db.relationship('Group', secondary=groups,
backref=db.backref('members', lazy='dynamic'))
class Group(db.Model):
__tablename__ = 'groups'
id = db.Column(db.Integer, primary_key=True)
Attempting to add the group to the database:
g = Group()
db.session.add(g)
u = User.query.filter_by(googleID=session.get('id')).first()
g.members.append(u)
db.session.commit()
Any help is appreciated!
So I fixed it... The issue was that I was importing the database variable (db) into my model file. Apparently that's a no-no. So I just copy/pasted it into the main file.

What's the proper way to describe an associative object by SQLalchemy the declarative way

I'm looking for a way to describe an associative object the declarative way. Beyond storing the foreign keys in the association table, I need to store information like the creation date of the association.
Today, my model looks like that :
# Define the User class
class User(Base):
__tablename__ = 'users'
# Define User fields
id = schema.Column(types.Integer(unsigned=True),
schema.Sequence('users_seq_id', optional=True), primary_key=True)
password = schema.Column(types.Unicode(64), nullable=False)
# Define the UserSubset class
class UserSubset(Base):
__tablename__ = 'subsets'
# Define UserSubset fields
id = schema.Column(types.Integer(unsigned=True),
schema.Sequence('subsets_seq_id', optional=True), primary_key=True)
some_short_description = schema.Column(types.Unicode(50), nullable=False)
# Define the subset memberships table
subset_memberships = schema.Table('group_memberships', Base.metadata,
schema.Column('user_id', types.Integer(unsigned=True), ForeignKey('users.id')),
schema.Column('subset_id', types.Integer(unsigned=True), ForeignKey('subsets.id')),
schema.Column('created', types.DateTime(), default=now, nullable=False),
)
Can I connect everything in an associative object ? Or should I change stop using the declarative way ?
What you are using at the moment is just a Many-to-Many-relation. How to work with association objects is described in the docs.
There is also an extension called associationproxy which simplifies the relation.
As you can see in the manual, configuring a one to many relation is really simple:
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50))
addresses = relation("Address", backref="user")
class Address(Base):
__tablename__ = 'addresses'
id = Column(Integer, primary_key=True)
email = Column(String(50))
user_id = Column(Integer, ForeignKey('users.id'))
Many to many relations isn't much harder:
There’s nothing special about many-to-many with declarative. The secondary argument to relation() still requires a Table object, not a declarative class. The Table should share the same MetaData object used by the declarative base:
keywords = Table('keywords', Base.metadata,
Column('author_id', Integer, ForeignKey('authors.id')),
Column('keyword_id', Integer, ForeignKey('keywords.id'))
)
class Author(Base):
__tablename__ = 'authors'
id = Column(Integer, primary_key=True)
keywords = relation("Keyword", secondary=keywords)
You should generally not map a class and also specify its table in a many-to-many relation, since the ORM may issue duplicate INSERT and DELETE statements.
Anyway, what you seem to be doing might be better served with inheritance. Of course, there can be complex table relations that will be a pathological case for the declarative way, but this doesn't seem to be one of them.
One more thing, code comments should state what the following code does ans why, not how it does it. Having a # Define the User class comment is almost like having a line of code saying a = 1 # assing value 1 to variable "a".

Categories

Resources