Context/Minimal Example: I'm relatively new to Database design and trying to design a small app in Flask/Flask_SQLAlchemy that tracks inventory.
I have a User table:
class Users(db.Model):
user_id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.String(25))
items = db.relationship('Item', lazy="dynamic")
and a Item table:
class Item(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('users.user_id'), index=True)
sku = db.Column(db.Integer, index=True, autoincrement=True)
name = db.String(10)
I would like to create a system where the Item.sku is unique... but only based on the user.id.
E.g. two users can have items of the same Sku, but one user may not have multiple items of the same sku. (And preferably have the sku automatically increment itself).
To me, this is a constraint that makes sense- sku+user_id should always be a unique combination, so I can save space and simplicity by using it as a primary key, as well as increasing the ?normalization? of the database.
However, I've spent a fair amount of time now reading and trying to figure out how to do this and I keep running into problems. Is there an easy way of accomplishing this, or is there something wrong with my logic that has lead to this design? Are there downsides to this I'm missing?
So far I've tried:
Setting both user_id and sku to primary_key=true
Setting them both to index=True (as you can see here)
Adding a table_args = db.PrimaryKeyConstraint (As discussed here https://www.reddit.com/r/flask/comments/g3tje5/composite_key_for_flasksqlalchemy/)
From what I've read the term of what I'm trying to accomplish here is a compound primary key, and that flask_sqlalchemy does support it, but with all of these I get exceptions that a constraint is failing or a parameter is missing.
Thanks for any help or advice you can provide.
Yes, a composite PK on (user_id, sku) will work, as in this example using vanilla SQLAlchemy ORM:
import sqlalchemy as db
from sqlalchemy.orm import declarative_base, relationship, Session
engine = db.create_engine("sqlite://")
Base = declarative_base()
class Users(Base):
__tablename__ = "users"
user_id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.String(25))
items = relationship('Item', lazy="dynamic")
class Item(Base):
__tablename__ = "item"
user_id = db.Column(db.Integer, db.ForeignKey('users.user_id'), primary_key=True)
sku = db.Column(db.Integer, index=True, primary_key=True)
name = db.String(10)
Base.metadata.create_all(engine)
with Session(engine) as sess:
gord = Users(first_name="Gord", items=[Item(sku=1)])
anne = Users(first_name="Anne", items=[Item(sku=1), Item(sku=2)])
sess.add_all([gord, anne])
sess.commit()
# okay so far
# now try to add a duplicate
gord.items.append(Item(sku=1))
sess.flush()
"""
sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: item.user_id, item.sku
[SQL: INSERT INTO item (user_id, sku) VALUES (?, ?)]
[parameters: (1, 1)]
"""
Related
Thank you in advance! I am working on a client relationship management app and am trying to configure the service level agreement models for meetings and calls. Both of these will share a many-to-many relationship with the model Month which is just the months of the year. Below is my code and the errors I am getting below that.
months = db.Table(
"months",
db.Column("month_id", db.Integer, db.ForeignKey("month.id"), primary_key=True),
db.Column("slacall_id", db.Integer, db.ForeignKey("sla_call.id"), primary_key=True),
db.Column(
"slameeting_id", db.Integer, db.ForeignKey("sla_meeting.id"), primary_key=True
),
)
class SLACall(db.Model):
id = db.Column(db.Integer, primary_key=True)
per_year = db.Column(db.Integer, nullable=False)
months = db.relationship(
"Month",
secondary=months,
lazy="subquery",
backref=db.backref("slacalls", lazy=True),
)
relationship_id = db.relationship("Relationship", backref="sla_call", lazy=True)
class SLAMeeting(db.Model):
id = db.Column(db.Integer, primary_key=True)
per_year = db.Column(db.Integer, nullable=False)
months = db.relationship(
"Month",
secondary=months,
lazy="subquery",
backref=db.backref("slameetings", lazy=True),
)
relationship_id = db.relationship("Relationship", backref="sla_meeting", lazy=True)
class Month(db.Model):
id = db.Column(db.Integer, primary_key=True)
month_name = db.Column(db.String(9), unique=True, nullable=False)
And I am getting the following error when I boot up flask for both SLA models (only one of the errors shown):
/home/kendall/Python/pm_crm/.venv/lib/python3.9/site-packages/flask_sqlalchemy/init.py:550: SAWarning: relationship 'Month.slameetings' will copy column month.id to column months.month_id, which conflicts with relationship(s): 'Month.slacalls' (copies month.id to months.month_id), 'SLACall.months' (copies month.id to months.month_id). If this is not the intention, consider if these relationships should be linked with back_populates, or if viewonly=True should be applied to one or more if they are read-only. For the less common case that foreign key constraints are partially overlapping, the orm.foreign() annotation can be used to isolate the columns that should be written towards. To silence this warning, add the parameter 'overlaps="months,slacalls"' to the 'Month.slameetings' relationship. (Background on this error at: https://sqlalche.me/e/14/qzyx)
Is this not the correct way to set this up? Or are multiple many-to-many relationships to a single model not possible?
Thank you again,
Kendall
I have two sqlalchemy models with a one-to-many relationship using lazy="dynamic like so:
class ProjectModel(db.Model):
__tablename__ = "projects"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(1000), nullable=False, unique=True)
jobs = db.relationship("JobModel", backref="project", cascade="all,delete", lazy="dynamic")
class JobModel(db.Model):
id = db.Column(db.Integer, primary_key=True)
When I deserialize a project to serve on my viewhandler, I want to run a filter on the jobs, something like so (which results in an error):
project = db.session.query(ProjectModel.first())
project.jobs = project.jobs.filter(JobModel.id == "36")
project_dump = ProjectSchema().dump(project)
The filter itself works fine, but I can't replace the jobs key in the Model object, is there another way I can run this filter so it applies when passed into a Marshmellow schema?
I am building an admin dashboard for my web app using Flask-Admin. For the user/address relationship, I am using a one to one relationship. On the user edit form, I'd like to be able to edit the individual components of the address (i.e. street address, city or zip) similar to what inline_models provides. Instead, flask-admin generates a select field and only allows me to select a different addresses.
I tried using inline_models = ['address'] in the UserModelView definition. However, I got the address object not iterable error due to the user/address relationship being configured to uselist=False. Switching uselist to True would affect other parts of my code, so I'd prefer to leave it as False.
From looking in flask-admin/contrib/sqla/forms, within the function get_forms, its being assigned a one to many tag which is what drives the use of a select field.
Before diving in further, I figured it best to see if anyone else has come across this or has a recommended fix/workaround.
models.py
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64))
address = db.relationship("Address", backref="user",
cascade="all, delete-orphan", lazy=False,
uselist=False, passive_deletes=True)
class Address(db.Model):
id = db.Column(db.Integer, primary_key=True)
line1 = db.Column(db.String(128))
zip = db.Column(db.String(20), index=True)
city = db.Column(db.String(64), index=True, nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey("user.id",
ondelete="CASCADE"))
admin.py
class UserModelView(ModelView):
column_list = [User.username, 'address']
form_columns = (User.username, 'address')
admin = Admin(name='Ask', template_mode='bootstrap3')
admin.add_view(UserModelView(User, db.session))
You can create 2 relations
# Relation for flask admin inline model
address_cms_relationsip = db.relationship(
"Address", backref="user", cascade="all, delete-orphan", lazy=False,
uselist=True, passive_deletes=True)
address_relationship = db.relationship(
"Address", cascade="all, delete-orphan", lazy=False,
uselist=False, passive_deletes=True)
#property
def address(self):
return self.address_relationship
In your code you can use property address
user: User # some User object
user.address.city
I have this many-to-many relationship that works correctly. However, now I need to have another class with a relation to this many-to-many.
currencies = db.Table('currencies_many',
db.Column('id', db.Integer, primary_key=True),
db.Column('currency_id', db.Integer, db.ForeignKey('currencies.id')),
db.Column('bank_id', db.Integer, db.ForeignKey('banks.id'))
)
class Bank(db.Model):
__tablename__ = 'banks'
id = db.Column(db.Integer, primary_key=True)
bank_name = db.Column(db.String(300))
currencies = db.relationship('Currency', secondary=currencies,
backref=db.backref('banks', lazy='dynamic'))
class Currency(db.Model):
__tablename__ = 'currencies'
id = db.Column(db.Integer, primary_key=True)
currency_name = db.Column(db.String(300))
What I mean is, for example, an order, I need to have the association to many to many.
class Order(db.Model):
__tablename__ = 'orders'
id = db.Column(db.Integer, primary_key=True)
bank_currency_identification = db.Column(db.Integer, db.ForeignKey('currencies_many.id'))
How can I do that? In my example I don't have db.relationship for bank_currency_identification, it is correct?
So if I understand your question correctly, you want to reference the currencies_many table from your orders table. If so, you are correct in having a foreign key relationship with the currencies_many table.
However, down the road you may come into some trouble when you want to query orders from your banks table. I would suggest, although it seems redundant, to create a one-to-many relationship between Order and Bank as well as between Order and Currency.
bank_id = db.Column(db.Integer, db.ForeignKey('bank.id'))
currency_id = db.Column(db.Integer, db.ForeignKey('currency.id'))
And then in the Bank class
orders = db.relationship('Order', backref='bank')
This gives you a much cleaner querying interface.
bank_orders = bank.orders
As well as makes your data model cleaner. It would be awkward to have to query orders from an intermediate table that also houses the currency. Just my two cents, but having an easy to understand Data model is better than making awkward relationships to save some redundancy.
I have these two tables in (1 to 0..1) relationship:
models.py
# --------------------------------------------------
class Person(db.Model):
__tablename__ = 'person'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, nullable=False)
address = db.Column(db.String, nullable=False)
# --------------------------------------------------
class Gift(db.Model):
__tablename__ = 'gift'
id = db.Column(db.Integer, primary_key=True)
person_id = db.Column(db.Integer, db.ForeignKey('person.id'))
person = relationship("Person", uselist=False, backref="gift")
gift_idea = db.Column(db.String, nullable=False)
# --------------------------------------------------
Questions:
Is the structure of my one to one tables correct? I know that in the SQLAlchemy documentation, the relationship code line (person = relationship...) is in the parent class but I need it to be in the child class, is that OK?!
My goal is to display (gift_idea from Gift table) and (person_name, person_address from Person table), if there's a gift_idea for that person. In my search I found people use query.join to get that and I have tried many codes but this following statement at least get me half the content I
need:
views.py
a = session.query(Gift).join(Person)
Could you please assist me with my questions? Thank you for any effort.
As per sqlalchemy One to One relationship pattern definition ...
One To One is essentially a bidirectional relationship with a scalar
attribute on both sides. To achieve this, the uselist flag indicates
the placement of a scalar attribute instead of a collection on the
“many” side of the relationship
In your case you've disabled "uselist" for Gifts by giving "uselist=False" (Actually by default it gets disabled). But you didn't disabled "uselist" for Persons. because of this Person can still hold list of Gifts (One person - Many Gifts).
You can get one to one mapping in two ways here..
1: By using backref function which provides arguments for the reverse side. keep your person as it is.
class Gift(db.Model):
__tablename__ = 'gift'
id = db.Column(db.Integer, primary_key=True)
person_id = db.Column(db.Integer, db.ForeignKey('person.id'))
person = relationship("Person", backref=backref("gift", uselist=False))
gift_idea = db.Column(db.String, nullable=False)
2: Adding scalar attribute on both the sides instead of using backref property.
class Gift(db.Model):
__tablename__ = 'gift'
id = db.Column(db.Integer, primary_key=True)
person_id = db.Column(db.Integer, db.ForeignKey('person.id'))
gift_idea = db.Column(db.String, nullable=False)
person = relationship("Person")
# -----------------------------------------------
class Person(db.Model):
__tablename__ = 'person'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, nullable=False)
address = db.Column(db.String, nullable=False)
gift = relationship("Gift", uselist=False)
Solution for Question-2:
#val: Gave you straightforward solution for this.. you could also try using lazy loading instead of clearly writing everything over orm query.
list_of_gift_object = session.query(Gift).join(Person).all()
may be you can iterate over the list and use Gift person attribute to get associated person data.
for gift in list_of_gift_object:
print gift.gift_idea, gift.person.name, gift.person.address
In order to define proper 1-to-[0..1] relationship from the child side, you must make sure that the backref has uselist=False set, while the uselist from child to parent will be False by default:
person = db.relationship(
"Person",
backref=db.backref("gift", uselist=False),
)
Then the query is very straightforward to do:
a = (session
.query(
Gift.gift_idea,
Person.name.label("person_name"),
Person.address.label("person_address"),
)
# .select_from(Gift) # #note: optional, but will need it if the first column in the `.query(...)` above will be from table `Person`
.join(Person)
)