sqlalchemy: non-trivial one-to-one relationship with multiple tables - python

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

Sqlalchemy double association table?

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)

How to establish bidirectional relationships on tables with multiple join paths in SQLALchemy?

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.

Relationships in SQLAlchemy are not working as expected

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).

SqlAlchemy relationship many-to-many to an other many-to-many

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.

Flask-SQLAlchemy Many-to-Many with composite key

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)

Categories