I'm struggling with creating a database model for my Flask application.
Let's consider I have few product types and due to nature of my application I want to have separate tables for each product, while keeping generic properties in a common table:
Example:
products table
id type name price
1 'motorcycle' 'Harley' 10000.00
2 'book' 'Bible' 9.99
motorcycles table
id manufacturer model max_speed
1 'Harley-Davidson' 'Night Rod Special' 150
books table
id author pages
2 'Some random dude' 666
Things to consider:
all tables have one-to-one relationship
having motorcycle_id, book_id, etc_id in products table is not an option
having product_id in product tables is acceptable
two-way relationship
How can I declare such a relationship?
You are looking for joined table inheritance. The base class and each subclass each create their own table, each subclass has a foreign key primary key pointing to the base table. SQLAlchemy will automatically handle the joining whether you query the base or sub class.
Here is a working example for some products:
from decimal import Decimal
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey, Numeric
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
engine = create_engine('sqlite:///:memory:', echo=True)
session = sessionmaker(bind=engine)()
Base = declarative_base(bind=engine)
class Product(Base):
__tablename__ = 'product'
id = Column(Integer, primary_key=True)
type = Column(String, nullable=False)
name = Column(String, nullable=False, default='')
price = Column(Numeric(7, 2), nullable=False, default=Decimal(0.0))
__mapper_args__ = {
'polymorphic_on': type, # subclasses will each have a unique type
}
class Motorcycle(Product):
__tablename__ = 'motorcycle'
# id is still primary key, but also foreign key to base class
id = Column(Integer, ForeignKey(Product.id), primary_key=True)
manufacturer = Column(String, nullable=False, default='')
model = Column(String, nullable=False, default='')
max_speed = Column(Integer, nullable=False, default=0)
__mapper_args__ = {
'polymorphic_identity': 'motorcycle', # unique type for subclass
}
class Book(Product):
__tablename__ = 'book'
id = Column(Integer, ForeignKey(Product.id), primary_key=True)
author = Column(String, nullable=False, default='')
pages = Column(Integer, nullable=False, default=0)
__mapper_args__ = {
'polymorphic_identity': 'book',
}
Base.metadata.create_all()
# insert some products
session.add(Book())
session.add(Motorcycle())
session.commit()
print(session.query(Product).count()) # 2 products
print(session.query(Book).count()) # 1 book
Related
I would like to create an association between a Dataset object and all Category objects through the Annotation table.
A Dataset contains a collection of Annotations. Each Annotation has a single Category. I want Dataset.categories to contain the unique set of Categories made up of all the Categories of all the Annotations in that Dataset instance. I have tried doing this with a double association table (dataset_categories), but it is not working. What is the right way to do this? Here is my code so far:
Base = declarative_base()
dataset_categories = Table('dataset_categories', Base.metadata,
Column('dataset_id', Integer, ForeignKey('datasets.id')),
Column('annotation_id', Integer, ForeignKey('annotations.id')),
Column('category_id', Integer, ForeignKey('categories.id')))
class Dataset(Base):
__tablename__ = 'datasets'
id = Column(Integer, primary_key=True)
annotations = relationship("Annotation")
categories = relationship("Category", secondary=dataset_categories)
class Annotation(Base):
__tablename__ = 'annotations'
id = Column(Integer, primary_key=True)
category_id = Column(Integer, ForeignKey('categories.id'), nullable=False)
category = relationship("Category")
dataset_id = Column(Integer, ForeignKey('datasets.id'))
class Category(Base):
__tablename__ = 'categories'
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False, unique=True)
dataset = relationship("Dataset", secondary=dataset_categories)
dataset_id = Column(Integer, ForeignKey('datasets.id'),
back_populates='categories')
Without the requirement that the association contain only the unique categories this would be as simple as using an association_proxy. One option is to define the collection class to use as set when defining the relationship:
class Dataset(Base):
__tablename__ = 'datasets'
id = Column(Integer, primary_key=True)
annotations = relationship("Annotation")
categories = relationship("Category", secondary="annotations", collection_class=set)
On the other hand the secondary table of a relationship does not have to be a base table, and so a simple select from annotations can be used:
class Dataset(Base):
__tablename__ = 'datasets'
id = Column(Integer, primary_key=True)
annotations = relationship("Annotation")
categories = relationship("Category",
secondary="""select([annotations.c.dataset_id,
annotations.c.category_id]).\\
distinct().\\
alias()""",
viewonly=True)
The example given in sqlalchemy documentation is,
from sqlalchemy import Integer, ForeignKey, String, Column
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class Customer(Base):
__tablename__ = 'customer'
id = Column(Integer, primary_key=True)
name = Column(String)
billing_address_id = Column(Integer, ForeignKey("address.id"))
shipping_address_id = Column(Integer, ForeignKey("address.id"))
billing_address = relationship("Address", foreign_keys=[billing_address_id])
shipping_address = relationship("Address", foreign_keys=[shipping_address_id])
class Address(Base):
__tablename__ = 'address'
id = Column(Integer, primary_key=True)
street = Column(String)
city = Column(String)
state = Column(String)
zip = Column(String)
I am trying a similar example (cannot place so much code here) it does not work if I do something similar to this:
from sqlalchemy import Integer, ForeignKey, String, Column
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class Customer(Base):
__tablename__ = 'customer'
id = Column(Integer, primary_key=True)
name = Column(String)
billing_address_id = Column(Integer, ForeignKey("address.id"))
shipping_address_id = Column(Integer, ForeignKey("address.id"))
billing_address = relationship("Address", foreign_keys=[billing_address_id], back_populates('bill_addr'))
shipping_address = relationship("Address", foreign_keys=[shipping_address_id], back_populates('ship_addr'))
class Address(Base):
__tablename__ = 'address'
id = Column(Integer, primary_key=True)
street = Column(String)
city = Column(String)
state = Column(String)
zip = Column(String)
bill_addr = relationship("Customer", back_populates('billing_address'))
ship_addr = relationship("Customer", back_populates('shipping_address'))
I have two doubts:
Q1) Is the above relationship bidirectional?
Q2) How to establish a bidirectional relationship between tables with multiple join paths?
edit:
In my case I am getting the following error:
sqlalchemy.exc.AmbiguousForeignKeysError
sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables
on relationship User.expenses - there are multiple foreign key paths linking the tables.
Specify the 'foreign_keys' argument, providing a list of those columns which should be counted as containing a
foreign key reference to the parent table.
It is working if I used 'backref' instead of 'back_populates'. I placed the 'backref' in the relationships on the side where both the foreign keys are present and removed the relationships on the other table.
I am trying to create a relationship between two tables; event and location.
The thing I am trying to accomplish is to create a one to many relationship. A event can be given at a location, so that location must be able to hold various Events. I have been fiddling with foreign keys for hours and I cannot figure out how to get it working.
My location.py looks like this:
from datetime import datetime
from sqlalchemy import Column, Integer, ForeignKey, Float, Date, String
from sqlalchemy.orm import relationship
from model import Base
class Location(Base):
__tablename__ = 'location'
id = Column(Integer, primary_key=True, nullable=False)
title = Column(String, unique=True)
description = Column(String, nullable=False)
founder_id = Column(Integer, ForeignKey('users.id'))
latitude = Column(Float)
longtitude = Column(Float)
creation_date = Column(Date, default=datetime.now)
# Relationships
creator = relationship('User', foreign_keys=founder_id)
events = relationship('Event', back_populates='location')
and the event.py looks like this:
from datetime import datetime
from sqlalchemy import Column, Integer, Date, Float, String, ForeignKey
from sqlalchemy.orm import relationship
from model import Base
from model.event_contestant import EventContestant
from model.location import Location
class Event(Base):
__tablename__ = 'event'
id = Column(Integer, primary_key=True)
title = Column(String, nullable=False, unique=True)
date = Column(Date, nullable=False)
location_id = Column(Integer),
longtitude = Column(Float, nullable=False)
latitude = Column(Float, nullable=False)
description = Column(String, nullable=False)
difficulty = Column(Integer, nullable=False, default=3)
creation_date = Column(Date, nullable=False, default=datetime.now)
event_creator = Column(Integer, ForeignKey('users.id'), nullable=False)
event_type = Column(String, nullable=False, default='Freestyle')
# Relationships
creator = relationship("User")
contestants = relationship(EventContestant, back_populates="event")
location = relationship('Location', foreign_key=location_id)
The stactrace I get from sqlalchemy looks like the following:
sqlalchemy.exc.InvalidRequestError: One or more mappers failed to initialize - can't proceed with initialization of other mappers. Triggering mapper: 'Mapper|Location|location'. Original exception was: Could not determine join condition between parent/child tables on relationship Location.events - there are no foreign keys linking these tables. Ensure that referencing columns are associated with a ForeignKey or ForeignKeyConstraint, or specify a 'primaryjoin' expression.
I'm truly puzzled and can't find out why this one to many should just work.
Event needs a ForeignKey pointing to Location in order to construct the relationship. Specifically, you want to change the definition of Event.location_id to add ForeignKey('location.id') (and get rid of that trailing comma; Event.location_id is currently a tuple).
I have the following mapped class, which is an association coming from 2 other classes.
class InstanceCustomer(Base):
__tablename__ = 'Instances_Customers_Association'
cust_id = Column(Integer, ForeignKey('Customers.id'), primary_key=True)
inst_id = Column(Integer, ForeignKey('Instances.id'), primary_key=True)
customer = relationship(Customer, backref=backref('customer'))
instance = relationship(Instance, backref=backref('instance'))
def __init__(self, cust_id=None, inst_id=None):
self.cust_id = cust_id
self.inst_id = inst_id
def __repr__(self):
return "<InstanceCustomer(cust_id='%s', inst_id='%s')>" % (self.cust_id, self.inst_id)
I would like to associate it to the class Person. So as 1 InstanceCustomer can have many Person and 1 Person can have many Instance Customer, I will need an other association between them, how can i do that? Is the primary-key/Foreign-key a problem as well?
Here is the class Person
class Person(Base):
__tablename__ = 'person'
id = Column( Integer, primary_key=True)
Is a N:N relationship, you need a cross relationship table. A example:
Class A(Base):
id = Column( Integer, primary_key=True)
Class B(Base):
id = Column( Integer, primary_key=True)
As = relationship(
'A',
secondary=AnB,
backref=backref('Bs')
)
AnB = Table(
"table_a_to_b",
Base.metadata,
Column(
'a_id',
Integer,
ForeignKey('A.id')
),
Column(
'b_id',
Integer,
ForeignKey('B.id')
)
)
Sqlalchemy doc for reference.
I am trying to build a Many-to-Many relationship using Flask-SQLAlchemy using two primary keys from one model and one from another. My models are the following:
Service:
class Service(db.Model):
"""
Service model object
"""
name = db.Column(db.String(120), primary_key=True, nullable=False)
description = db.Column(db.Text)
ContactClosure:
class ContactClosure(db.Model):
module = db.Column(db.Integer, primary_key=True, nullable=False)
relay = db.Column(db.Integer, primary_key=True, nullable=False)
status = db.Column(db.Boolean)
description = db.Column(db.Text)
#Relationships
hostname = db.Column(db.String(120), db.ForeignKey('ip2cc.hostname'), primary_key=True, nullable=False)
device_name = db.Column(db.String(120), db.ForeignKey('device.name'), primary_key=True, nullable=False)
services = db.relationship('Service', secondary=cc_services, backref=db.backref('contact_closures', lazy='dynamic'))
This is the related table:
cc_services = db.Table('cc_services',
db.Column('service_name', db.String(120), db.ForeignKey('service.name')),
db.Column('hostname', db.String(120), db.ForeignKey('contact_closure.hostname')),
db.Column('device_name', db.String(120), db.ForeignKey('contact_closure.device_name')),
)
And this is the error I am getting:
"AmbiguousForeignKeysError: Could not determine join condition between
parent/child tables on relationship ContactClosure.services - there
are multiple foreign key paths linking the tables via secondary table
'cc_services'. Specify the 'foreign_keys' argument, providing a list
of those columns which should be counted as containing a foreign key
reference from the secondary table to each of the parent and child
tables."
If anybody can tell what is the problem here I will be highly thankful, I've been stuck on this for a while...
Ok, finally I found the solution to this problem:
cc_services = db.Table('cc_services',
db.Column('service_name', db.String(120),
db.ForeignKey('service.name')),
db.Column('cc_hostname', db.String(120)),
db.Column('cc_module', db.Integer),
db.Column('cc_relay', db.Integer),
ForeignKeyConstraint(
('cc_hostname', 'cc_module', 'cc_relay'),
('contact_closure.hostname', 'contact_closure.module', 'contact_closure.relay')
)
)
If a model has multiple keys, they must be declared on the helper table on a ForeignKeyConstraint statement.
You need to define 'foreign_keys' argument as the error text says, like this:
services = db.relationship('Service', secondary=cc_services, backref=db.backref('contact_closures', lazy='dynamic'), foreign_keys=[column_name])
If you're using an association table or fully declared table metadata, you can use the primary_key=True in both columns, as suggested here.
Association table example:
employee_role = db.Table(
"employee_role",
db.Column("role_id", db.Integer, db.ForeignKey("role.id"), primary_key=True),
db.Column("employee_id", db.Integer, db.ForeignKey("agent.id"), primary_key=True),
)
Metadata example:
# this is using SQLAlchemy
class EmployeeRole(Base):
__tablename__ = "employee_role"
role_id = Column(Integer, primary_key=True)
employee_id = Column(Integer, primary_key=True)
# this is using Flask-SQLAlchemy with factory pattern, db gives you access to all SQLAlchemy stuff
class EmployeeRole(db.Model):
__tablename__ = "employee_role"
role_id = db.Column(db.Integer, primary_key=True)
employee_id = db.Column(db.Integer, primary_key=True)
Alembic migration for it:
op.create_table(
'employee_role',
sa.Column('role_id', sa.Integer(), nullable=False),
sa.Column('employee_id', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('role_id', 'employee_id')
)
SQL:
CREATE TABLE agent_role (
role_id INTEGER NOT NULL,
employee_id INTEGER NOT NULL,
PRIMARY KEY (role_id, employee_id)
);
In terms of relationship, declare it on one side (this should give you role.employees or employee.roles which should return a list):
# this is using Flask-SQLAlchemy with factory pattern, db gives you access to all SQLAlchemy stuff
class Employee(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
roles = db.relationship("Role", secondary=employee_role, backref="employee")
Your Role class can be:
# this is using Flask-SQLAlchemy with factory pattern, db gives you access to all SQLAlchemy stuff
class Role(db.Model):
__tablename__ = "role"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(25), nullable=False, unique=True)