I have a many to many relationship in my DB. The problem is that the many to many table is not populated when I submit my form.
That is my code for my models:
CompanyCategory = Table('CompanyCategory', Base.metadata,
Column('id', Integer, primary_key=True),
Column('categoryId', Integer, ForeignKey('categories.id')),
Column('companyId', Integer, ForeignKey('companies.id')))
class Company(Base):
__tablename__ = 'companies'
id = Column(Integer, primary_key=True)
company_name = Column(Text, nullable=False)
users_id = Column(Integer, ForeignKey('users.id'))
users = relationship("User", backref=backref('users', cascade="all, delete-orphan"),
lazy='joined')
# foreign key for category
addresses_category = relationship('Category', secondary=CompanyCategory, backref='companies')
def __init__(self, company_name=None, users_id=None):
self.company_name = company_name
self.users_id = users_id
def get(self, id):
if self.id == id:
return self
else:
return None
def __repr__(self):
return '<%s(%r, %r, %r)>' % (self.__class__.__name__, self.id, self.company_name, self.users_id)
class Category(Base):
__tablename__ = 'categories'
id = Column(Integer, primary_key=True)
category_name = Column(Text, nullable=False)
addresses_company = relationship('Company', secondary=CompanyCategory, backref='categories')
def __init__(self, category_name=None):
self.category_name = category_name
def get(self, id):
if self.id == id:
return self
else:
return None
def __repr__(self):
return '<%s(%r, %r)>' % (self.__class__.__name__, self.id, self.category_name)
I have two classes Company and Category. A company can have multiple categories (later there will be a fixed number of categories which I will create).
To show this I created a simple many to many relation CompanyCategory.
As far as I understand sqlalchemy should automatically add the foreign keys of company and categorie if these are created.
That is the part where I am adding a company and a category to the DB:
new_user_company = Company(company_name=form.company_name.data.strip(), users_id = new_user.id)
if new_user_company:
db_session.add(new_user_company)
db_session.commit()
new_user_company_category = Category(category_name=form.category_name.data)
if new_user_company_category:
db_session.add(new_user_company_category)
db_session.commit()
In this case the data is added to Company and also to Category but the there is no entry to CompanyCategory.
I have already tried a few solutions here but they are mostly for flask-sqlalchemy and not for bidirectional behavior. I need bidirectional behavior because if a company is deleted all many to many relations where the company occures should be also deleted. Also if a category will be deleted (wont probably happen) all relations with companies where these category occured should be deleted.
Thanks
As SQLAlchemy ORM tutorial explains: you need to specify relationship attribute for an instance. In your case you need to set Company.addresses_category or Category.addresses_company:
new_user_company = Company(company_name=form.company_name.data.strip(), users_id = new_user.id)
if new_user_company:
db_session.add(new_user_company)
db_session.commit()
new_user_company_category = Category(category_name=form.category_name.data)
new_user_company_category.addresses_company.append(new_user_company)
if new_user_company_category:
db_session.add(new_user_company_category)
db_session.commit()
Also if you need the relations to be deleted if one of parent object is deleted it is better to set ondelete attributes:
CompanyCategory = Table('CompanyCategory', Base.metadata,
Column('id', Integer, primary_key=True),
Column('categoryId', Integer, ForeignKey('categories.id', ondelete='CASCADE')),
Column('companyId', Integer, ForeignKey('companies.id', ondelete='CASCADE')))
Related
I am trying to build a model where there is the default values then there is the user defined values. So the default values would come from the spices table. Yes the spices table would contain default data. The user would define the composite spice and make modifications as desired for a specific recipe. If you think I am structuring this wrong please provide your expertise. I feel lost on how to do this.
class User(db.Model, UserMixin):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=True)
#... extra
#... extra
class Spice(db.Model):
__tablename__ = 'spices'
code = db.Column(db.String(5), primary_key=True) # this is the id code
name = db.Column(db.String(60))
origin = db.Column(db.String(15))
def __init__(self, code, name, origin):
self.code = code
self.name = name
self.origin = origin
class Seasoning(Spice):
__tablename__ = 'seasonings'
# Note that the below item should come from Recipe. How would I do this?
recipe_id = db.Column(db.Integer, db.ForeignKey('recipe.id'), nullable=False)
class Recipe(db.Model):
__tablename__ = 'recipe'
user = db.relationship(User)
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(60))
description = db.Column(db.Text(), nullable=False)
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
ingredient = db.relationship('Ingredient', backref='recipe', lazy='dynamic', primaryjoin="Recipe.id == Seasoning.recipe_id")
def __init__(self, id, name, description, date):
self.id = id
self.name = name
self.description = description
self.date = date
in my views.py I have
...
seasoning = Seasoning(code=from.code.data, name=form.name.data, origin=form.origin,
recipe_id=recipe_id)
db.session.add(seasoning)
db.create_all()
db.session.commit()
...
When I run this I do get an error when I try to commit() to seasoning. How do I resolve this?
sqlalchemy.exc.OperationalError: (raised as a result of Query-invoked
autoflush; consider using a session.no_autoflush block if this flush
is occurring prematurely) (sqlite3.OperationalError) table spices has
no column named recipe_id
You need to describe recipe_id in your spices class
table spices has no column named recipe_id
Overview
I'm working on a project which stores Artists in a database. Each time one or more artists are added to the database, a "transaction" is created for which the user can remove to undo that particular batch. Finally, each user can define one or more "profiles", storing separate lists of Artists in each (defined in var active_profile).
Using FKs with ondelete="CASCADE", when a Profile is deleted, all transactions are deleted and subsequently any artists belonging to those transactions are deleted.
active_profile = 2
class Profile(Base):
__tablename__ = 'profile'
id = Column(Integer, primary_key=True)
name = Column(String)
def __init__(self, name):
self.name = name
class Transaction(Base):
__tablename__ = 'transaction'
id = Column(Integer, primary_key=True)
timestamp = Column(Integer, nullable=False)
profile_id = Column(
Integer,
ForeignKey('profile.id', ondelete="CASCADE"),
nullable=False,
)
artist = relationship(
"Artist",
cascade="delete",
back_populates='transaction',
)
def __init__(self):
self.timestamp = int(time.time())
self.profile_id = active_profile
class Artist(Base):
__tablename__ = 'artist'
id = Column(Integer, primary_key=True)
art_id = Column(Integer)
art_name = Column(String)
txn_id = Column(
Integer,
ForeignKey('transaction.id', ondelete="CASCADE"),
nullable=False,
)
transaction = relationship("Transaction", back_populates="artist")
def __init__(self, art_id, art_name, txn_id):
self.art_id = art_id
self.art_name = art_name
self.txn_id = txn_id
The Goal
Most queries to the database will select where the profile_id is equal to the active_profile so I'm trying to figure out if there is a way to integrate this requirement into a relationship without having to specify it each time I query the database.
I have tried specifying primaryjoin in the relationship for the Transaction class but it seems to set the primaryjoin condition only once and doesn't change if active_profile is changed. I incorrectly assumed the relationship would be re-evaluated each time the class was called:
artist = relationship(
"Artist",
cascade="delete",
back_populates="transaction",
primaryjoin=f"and_(Transaction.id == Artist.txn_id, Transaction.profile_id == {active_profile}",
)
If this was re-evaluated each time, this would be exactly what I need.
Question
Is there a way to force the relationship to be re-evaluated and if not, is there any other option without the need to append .where(Transaction.profile_id == active_profile) to each query?
I am hoping for some guidance about what I believe is going to be a common pattern in SQLAlchemy for Python. However, I have so far failed to find a simple explanation for someone new to SQLAlchemy.
I have the follow objects:
Customers
Orders
Products
I am building a Python FastAPI application and I want to be able to create customers, and products individually. And subsequently, I want to then be able to create an order for a customer that can contain 1 or more products. A customer will be able to have multiple orders also.
Here are my SQLAlchemy models:
order_products = Table('order_products', Base.metadata,
Column('order_id', ForeignKey('orders.id'), primary_key=True),
Column('product_id', ForeignKey('products.id'), primary_key=True)
)
class Customer(Base):
__tablename__ = "customers"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
address = Column(String)
phonenumber = Column(String)
email = Column(String, unique=True, index=True)
is_active = Column(Boolean, default=True)
orders = relationship("Order", back_populates="customers")
class Order(Base):
__tablename__ = "orders"
id = Column(Integer, primary_key=True, index=True)
ordernumber = Column(String, index=True)
customer_id = Column(Integer, ForeignKey("customers.id"))
customers = relationship("Customer", back_populates="orders")
products = relationship("Product", secondary="order_products", back_populates="orders")
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
size = Column(Integer)
order_id = Column(Integer, ForeignKey("orders.id"))
orders = relationship("Order", secondary="order_products", back_populates="products")
And here are my CRUD operations:
def create_customer(db: Session, customer: customer.CustomerCreate):
db_customer = models.Customer(name = customer.name, address = customer.address, email=customer.email, phonenumber=customer.phonenumber)
db.add(db_customer)
db.commit()
db.refresh(db_customer)
return db_customer
def create_product(db: Session, product: product.Productreate):
db_product = models.Product(name = product.name, size = product.size)
db.add(db_product)
db.commit()
db.refresh(db_product)
return db_product
def create_order(db: Session, order: order.OrderCreate, cust_id: int):
db_order = models.Order(**order.dict(), customer_id=cust_id)
db.add(db_order)
db.commit()
db.refresh(db_order)
return db_order
def update_order_with_product(db: Session, order: order.Order):
db_order = db.query(models.Order).filter(models.Order.id==1).first()
if db_order is None:
return None
db_product = db.query(models.Order).filter(models.Product.id==1).first()
if db_order is None:
return None
db_order.products.append(db_product)
db.add(db_order)
db.commit()
db.refresh(db_order)
return db_order
All of the CRUD operations work apart from update_order_with_product which gives me this error:
child_impl = child_state.manager[key].impl
KeyError: 'orders'
I'm not sure if I am taking the correct approach to the pattern needed to define the relationships between my models. If not, can someone point me in the right direction of some good examples for a beginner?
If my pattern is valid then there must be an issue with my CRUD operation trying to create the relationships? Can anyone help with that?
This query could be a problem:
db_product = db.query(models.Order).filter(models.Product.id==1).first()
Should probably be:
db_product = db.query(models.Product).filter(models.Product.id==1).first()
because you want to get a Product instance, not Order.
When you update a record you should not add it to the session (because it has been registered to the session when you queried the record).
def update_order_with_product(db: Session, order: order.Order):
db_order = db.query(models.Order).filter(models.Order.id==1).first()
if db_order is None:
return None
db_product = db.query(models.Product).filter(models.Product.id==1).first()
if db_product is None:
return None
db_order.products.append(db_product)
db.commit()
db.refresh(db_order)
return db_order
Context: I'm making an auctioning website for which I am using Flask-SQLAlchemy. My tables will need to have a many-to-many relationship (as one artpiece can have many user bids and a user can bid on many artpieces)
My question is: it is possible to add another column to my joining table to contain the id of the user bidding, the id of artpiece that they are bidding on and also how much they bid? Also if yes, how would I include this bid in the table when I add a record to said table?
bid_table = db.Table("bid_table",
db.Column("user_id", db.Integer, db.ForeignKey("user.user_id")),
db.Column("item_id", db.Integer, db.ForeignKey("artpiece.item_id"))
)
class User(db.Model):
user_id = db.Column(db.Integer, unique=True, primary_key=True, nullable=False)
username = db.Column(db.Integer, unique=True, nullable=False)
email = db.Column(db.String(50), unique =True, nullable=False)
password = db.Column(db.String(60), nullable=False)
creation_date = db.Column(db.DateTime, default=str(datetime.datetime.now()))
bids = db.relationship("Artpiece", secondary=bid_table, backref=db.backref("bids", lazy="dynamic"))
class Artpiece(db.Model):
item_id = db.Column(db.Integer, unique=True, primary_key=True, nullable=False)
artist = db.Column(db.String(40), nullable=False)
buyer = db.Column(db.String(40), nullable=False)
end_date = db.Column(db.String(40))
highest_bid = db.Column(db.String(40))
It is possible to do this with SQL Alchemy, but it's very cumbersome in my opinion.
SQLAlchemy uses a concept called an Association Proxy to turn a normal table into an association table. This table can have whatever data fields you want on it, but you have to manually tell SQLAlchemy which columns are foreign keys to the other two tables in question.
This is a good example from the documentation.
In your case, the UserKeyword table is the association proxy table that you want to build for your user/bid scenario.
The special_key column is the arbitrary data you would store like the bid amount.
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import backref, declarative_base, relationship
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String(64))
# association proxy of "user_keywords" collection
# to "keyword" attribute
keywords = association_proxy('user_keywords', 'keyword')
def __init__(self, name):
self.name = name
class UserKeyword(Base):
__tablename__ = 'user_keyword'
user_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
keyword_id = Column(Integer, ForeignKey('keyword.id'), primary_key=True)
special_key = Column(String(50))
# bidirectional attribute/collection of "user"/"user_keywords"
user = relationship(User,
backref=backref("user_keywords",
cascade="all, delete-orphan")
)
# reference to the "Keyword" object
keyword = relationship("Keyword")
def __init__(self, keyword=None, user=None, special_key=None):
self.user = user
self.keyword = keyword
self.special_key = special_key
class Keyword(Base):
__tablename__ = 'keyword'
id = Column(Integer, primary_key=True)
keyword = Column('keyword', String(64))
def __init__(self, keyword):
self.keyword = keyword
def __repr__(self):
return 'Keyword(%s)' % repr(self.keyword)
Check out the full documentation for instructions on how to access and create this kind of model.
Having used this in a real project, it's not particularly fun and if you can avoid it, I would recommend it.
https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
I have a many-to-many relationship between Categories and Products as follows:
category_product = db.Table('category_product',
db.Column('category_id',
db.Integer,
db.ForeignKey('category.id')),
db.Column('product_id',
db.Integer,
db.ForeignKey('product.id')))
class Product(db.Model):
""" SQLAlchemy Product Model """
id = db.Column(db.Integer, primary_key=True)
sku = db.Column(db.String(10), unique=True, nullable=False)
name = db.Column(db.String(80), nullable=False)
categories = db.relationship('Category', secondary=category_product,
backref=db.backref('categories',
lazy='dynamic'))
def __init__(self, name):
self.name = name
def __repr__(self):
return '<Product {}>'.format(self.name)
class Category(db.Model):
""" SQLAlchemy Category Model """
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), nullable=False)
products = db.relationship('Product', secondary=category_product,
backref=db.backref('products', lazy='dynamic'))
def __init__(self, name):
self.name = name
def __repr__(self):
return '<Category {}>'.format(self.name)
I am trying to get all the Product objects in a given Category, specified by category_id:
products = db.session.query(Category).\
filter_by(id=category_id).\
products.\
all()
However, I get the following exception:
AttributeError: 'Query' object has no attribute 'products'
I'm probably missing something simple.
You cannot follow the filter_by with the attribute name 'products'. You first need to catch the results using all() or first(). Also since you are using Flask-SQLAlchemy, I suggest not using db.session.query(Category) and instead Category.query. So change this
products = db.session.query(Category).\
filter_by(id=category_id).\
products.\
all()
to
all_products = Category.query.\
filter_by(id=category_id).\
first().\
products
For someone who need...
You can use .any()
product = Product.query.filter(Category.products.any(id=cat_id)).all()
I'm not test the query. But I think it's work...
Happy coding...