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

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.

Related

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

Working with back_populates fields in flask-admin and sqlalchemy

Consider a simple many-to-one model like this:
class Entity(Base):
__tablename__ = 'entity'
id = Column(Integer, Sequence('entity_seq'), primary_key=True)
name = Column(String(50), nullable=False)
persons = relationship('Person', back_populates='entity')
def __str__(self):
return self.name
class Person(Base):
__tablename__ = 'person'
id = Column(Integer, Sequence('person_seq'), primary_key=True)
entity_key = Column(ForeignKey('entity.id'), nullable=False)
last_name = Column(String(30), nullable=False)
first_name = Column(String(30), nullable=False)
entity = relationship('Entity', back_populates='persons')
def __str__(self):
return f'{self.first_name} {self.last_name}'
In other words, many persons belong to one entity.
If you use a flask-admin view like this:
admin.add_view(ModelView(Entity, db.session))
You might get a list like this:
Editing one of these entries can produce this output:
This presents some problems:
The persons field can be very large and take a long time to fill and probably needs to be paginated, but I can't find a way in flask-admin to cause that pagination.
Individual persons can be deleted (via the "x") but that violates the database nullable constraint on the column. It seems like flask-admin shouldn't allow that by default, or there should be a way to control it.
The persons are formatted via the __str__ attribute, but it may be necessary to format them some other way, but I can't find a way in flask-admin to do that.
What do you do in flask-admin to address these problems?

SQLalchemy built relationship between classes

I am running into a conceptual problem I do not know how to approach, which might be due my lack of knowledge with SQLalchemy. I have two classes: People and Person and I want them each to have a column to share their respective id's with each other using the relationship function.
Now, I have an endpoint in views.py which instantiates those two classes and establishes a Child / Parent relationship. Looking at the database results however, only People, the parent class has the id stored in its respective table, while the Person table in column people is None.
I know the id in person is only generated after the commit() statement and thus None for Person, and was wondering if there is a way to solve this elegantly, or do I need to first query the current people instance, retreive its id, set the id in the person table and then commit() again?
I hope my question makes sense,thank you.
'''
model.py
'''
class People(Model):
__tablename__ = 'people'
id = Column(Integer, primary_key=True)
person = relationship('Person', back_populates='people')
person_id = Column(Integer, ForeignKey('people.id'))
class Person(Model):
__tablename__ = 'people'
id = Column(Integer, primary_key=True)
people = relationship('People', uselist=False, back_populates='person')
'''
views.py
'''
#main.route('/', methods=['GET', 'POST'])
def index():
people = People()
person = Person(people_id = ?)
people.person = person
session.add(person)
session.add(people)
session.commit()
I regret that I have not yet understood your question. However, since your code contains some errors, I will first write you my corrected variant.
class People(Model):
__tablename__ = 'people'
id = Column(Integer, primary_key=True)
person = relationship('Person', back_populates='people')
person_id = Column(Integer, ForeignKey('person.id'))
class Person(Model):
__tablename__ = 'person'
id = Column(Integer, primary_key=True)
people = relationship('People', back_populates='person')
def index():
person = Person()
people = People()
people.person = person
session.add(person)
session.add(people)
session.commit()
The question of gittert seems justified to me. It makes no sense to save the ForeignKey in both tables on the referenced identifiers of the other model.
What do you want to achieve?
If you're looking for an actual column in your database for your 'relationships', you won't find them. Your .people and .person are virtual relationships created in Python without any interaction with the SQL database.

SQLAlchemy: Relationship Field Values

I have three [MySQL] tables: Person, Role, PersonRole
class Person(Base):
__tablename__ = 'people'
id = Column(INTEGER(unsigned=True), primary_key=True)
full_name = Column(String(120), nullable=False)
email = Column(String(128))
username = Column(String(50))
last_modified = Column(TIMESTAMP, nullable=False,
server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))
created = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'))
roles = relationship('Role',
secondary='person_role',
primaryjoin="and_(Person.id==PersonRole.person_id,"
"Role.active==True,"
"PersonRole.active==True)",
back_populates='people')
def __repr__(self):
"""String representation."""
return '''<Person(id='%s', full_name='%s')>''' % (
str(self.id), str(self.full_name)
)
class Role(Base):
__tablename__ = 'role'
id = Column(INTEGER(unsigned=True), Sequence('role_id_seq'), primary_key=True)
role_name = Column(String(16))
active = Column(Boolean, default=True)
last_modified = Column(TIMESTAMP, nullable=False,
server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))
created = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'))
users = relationship('Person',
secondary='person_role',
primaryjoin="and_(Role.id==PersonRole.role_id,"
"Role.active==True,"
"PersonRole.active==True)",
back_populates='roles')
def __repr__(self):
"""String representation."""
return '''<Role(role_id='%s', role_name='%s')>''' % (
str(self.role_id), str(self.role_name)
)
class PersonRole(Base):
__tablename__ = 'person_role'
ds_id = Column(INTEGER(unsigned=True),
ForeignKey('person.id'), primary_key=True)
role_id = Column(INTEGER(unsigned=True),
ForeignKey('role.id'), primary_key=True)
active = Column(Boolean, default=True)
last_modified = Column(TIMESTAMP, nullable=False,
server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))
created = Column(TIMESTAMP,
server_default=text('CURRENT_TIMESTAMP'))
Retrieving a Person like so:
...session setup as s...
person = s.query(Person).get(123)
and to list out their roles:
for role in person.roles:
print(role.id, role.role_name, role.active)
Now, in each table there is an active column. This is to keep track of an [shocker] active status as we dont want to remove data from the table, merely keep it's state. Now to the two issues which have kept me from using SQLAlchemy altogether and has me writing and executing SQL manually -
The active portion in the final loop though displays the active state of role.active and not that of the actual relationship, person_role.active.
Removing a role will remove the relationship row instead of performing the desired action, person_role.active = 0
Even if I deactivate the relationship, adding it again will set off the Duplicate Key error.
Is there a sane, valid way to go about accomplishing this without restructuring my data?
Edit for further clarification:
The two main tables in this case are person and person_role. Person is the main table which holds our users. PersonRole holds the roles a person actually has, in the form of person.id to role.id. The Role table is merely a lookup table for the Role definition (to get the names).
I suppose what I want is a way to intercept how the ORM actually adds/removes the data. Adding should [more-or-less] do an "upsert" and removing should basically run a update query like: update person_role set active = 0 where person_id = %s and role_id = %s.
I have read a lot of the docs but tend to get lost in the terminologies. :S
Not sure I completely understood your problems, but let me try to help you:
1) This is the one I am most confused with: person.roles maps to a collection of Role entities. Ain't that what you expect?
2, 3) You have set up a relationship between Person and Role, using PersonRole as secondary. Deactivating means setting PersonRole.status to inactive, not removing it. Trying to add it again will show a Duplicate Key error indeed!
I think you want to run a query to load a PersonRole entity by ds_id and role_id, update its status to active and persist changes. I understand that sometimes this may be tedious to perform, so maybe a immediate solution that would not require you to move data around would be to map Person and Role to PersonRole. So, in spite of having Person.roles you would have Person.personRoles, so you have access to PersonRole entities and may set its status.
Of course this is a very immediate solution. SQLAlchemy is extremely featured so maybe you can intercept the Person.roles removal and customize its behaviour to set PersonRole to inactive. You may want to read more about cascading on the docs as well.
Hope I was able to clarify things a little :)

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