Follwing the documentation, I have the following tables defined and populated but no relations have been defined yet.
class CountryAssociation(Base):
__tablename__ = 'Country_Region_Mapping'
country_id = Column(Integer, ForeignKey('countries.uid'), primary_key=True)
region_id = Column(Integer, ForeignKey('regions.uid'), primary_key=True)
region = relationship('Region', back_populates='country')
country = relationship('Countries', back_populates='region')
extra_data = Column(String(50))
class Countries(Base):
__tablename__ = 'countries'
uid = Column(Integer, primary_key=True)
countryname = Column('English_short_name',
String(255), unique=True, nullable=False)
region = relationship('CountryAssociation',
back_populates='country')
class Region(Base):
__tablename__ = 'regions'
uid = Column(Integer, primary_key=True)
region = Column(String(255), unique=True, nullable=False)
country = relationship('CountryAssociation',
back_populates='region')
I now want to create many to many relations between the tables. docs
Base = automap_base() #reflecting the orm way
engine = create_engine('sqlite:///mydatabse.db')
Base.prepare(engine, reflect=True)
Session = sessionmaker(bind=engine)
session = Session()
table_countries = Base.classes.countries
table_regions = Base.classes.regions
r = session.query(table_regions).filter(table_regions.region == "Western Europe").first()
c = session.query(table_countries).filter(table_countries.English_short_name == "Germany").first()
c.region.append(r) # this fails with
AttributeError: 'countries' object has no attribute 'region'
This however works:
c.countryname # Germany
I dont get what I am doing wrong here (beginner)...
Because you've used the association object pattern with extra_data the automap relationship detection will not recognize Country_Region_Mapping as a secondary table of a many to many:
If the table contains two and exactly two ForeignKeyConstraint objects, and all columns within this table are members of these two ForeignKeyConstraint objects, the table is assumed to be a “secondary” table, and will not be mapped directly.
Put another way: not all columns within Country_Region_Mapping are members of the foreign key constraints, so the table is not a secondary, so no many to many relationship is created.
Another thing you've overlooked is the naming scheme. If you had a working secondary table, then the created relationship would be named regions_collection by default (because of the plural table name).
What does happen is that the many to one / one to many relationships between regions and Country_Region_Mapping, and countries and Country_Region_Mapping are created:
In [22]: table_countries.country_region_mapping_collection
Out[22]: <sqlalchemy.orm.attributes.InstrumentedAttribute at 0x7f2d600fb258>
In [23]: table_regions.country_region_mapping_collection
Out[23]: <sqlalchemy.orm.attributes.InstrumentedAttribute at 0x7f2d600fbaf0>
In [28]: table_country_region_mapping.countries
Out[28]: <sqlalchemy.orm.attributes.InstrumentedAttribute at 0x7f2d535a2990>
In [29]: table_country_region_mapping.regions
Out[29]: <sqlalchemy.orm.attributes.InstrumentedAttribute at 0x7f2d535a2938>
Note that due to plural table naming the scalar relationship attributes on Country_Region_Mapping also have plural naming.
With these in mind you'd add a new association like so:
In [36]: c.country_region_mapping_collection.append(
...: Base.classes.Country_Region_Mapping(countries=c, regions=r))
Related
I can return the name of the table of which a foreign key references, but I want to do the opposite.
Suppose I have two tables, Users and Address, and Address has a foreign key to Users (one-to-many). I also made this bidirectional so that I can get the User from the address table.
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String)
address = relationship("Address", back_populates="user")
class Address(Base):
__tablename__ = 'address'
id = Column(Integer, primary_key=True, autoincrement=True)
company_name = Column(String)
user_id = Column(Integer, ForeignKey('user.id'))
user = relationship("User", back_populates="address")
If I wanted to figure out the name of the parent table, in a way that works for any other table in a similar relationship, I can to this:
for i in Address.__table__.foreign_key_constraints:
print(i.referred_table)
tab = Table(Address.__tablename__, Base.metadata, autoload_with=engine, extend_existing=True)
for i in tab.foreign_key_constraints:
print(i.referred_table)
#both output "user" which is the __tablename__ of User
This was me accessing the User table using the foreign key constraint attribute. But how do I do the opposite, using an attribute from User to access Address? I mainly want to know so I can handle relationships in a bulk core insert.
tab = Table(User.__tablename__, Base.metadata, autoload_with=engine, extend_existing=True)
records = df.to_dict(orient="records")
insert_stmt = sqlalchemy.dialects.sqlite.insert(tab).values(records)
#list of primary keys
pks = [pk.name for pk in tab.primary_key]
update_columns = {col.name: col for col in insert_stmt.excluded if col.name not in pks}
update_statement = insert_stmt.on_conflict_do_update(index_elements=pks, set_=update_columns)
The above works once I execute the on_conflict_do_update statement, but it does not generate any Address rows.
I found one way of doing it, but it's not as clean as I wanted.
tab = list(inspect(User).relationships)[0].entity.local_table
For any declarative table (classes), you can inspect them to get a Mapper Object. The Mapper Object has relationships as an attribute, which returns a collection of relationships.
inspect(User).relationships
#Get the relationship, note this will return the relationship attribute from the class
list(inspect(User).relationships)[0]
So I then get the mapper/class this relationship belongs to with .entity. Then from the declarative table, a table object is returned with the local_table attribute
entity = list(inspect(User).relationships)[0].entity
tab = entity.local_table
This statement should evaluate to True
tab == Address.__table__
I'm trying to implement a user-facing PreviewList of Articles, which will keep its size even if an Article is deleted. So if the list has four objects [1, 2, 3, 4] and one is deleted, I want it to contain [1, 2, None, 4].
I'm using a relationship with a secondary table. Currently, deleting either Article or PreviewList will delete the row in that table. I've experimented with cascade options, but they seem to affect the related items directly, not the contents of the secondary table.
The snippet below tests for the desired behaviour: deleting an Article should preserve the row in ArticlePreviewListAssociation, but deleting a PreviewList should delete it (and not the Article).
In the code below, deleting the Article will preserve the ArticlePreviewListAssociation, but pl.articles does not treat that as a list entry.
from db import DbSession, Base, init_db
from sqlalchemy import Column, String, Integer, ForeignKey
from sqlalchemy.orm import relationship
session = DbSession()
class Article(Base):
__tablename__ = 'articles'
id = Column(Integer, primary_key=True)
title = Column(String)
class PreviewList(Base):
__tablename__ = 'preview_lists'
id = Column(Integer, primary_key=True)
articles = relationship('Article', secondary='associations')
class ArticlePreviewListAssociation(Base):
__tablename__ = 'associations'
article_id = Column(Integer, ForeignKey('articles.id'), nullable=True)
previewlist_id = Column(Integer, ForeignKey('preview_lists.id'), primary_key=True)
article = relationship('Article')
preview_list = relationship('PreviewList')
init_db()
print(f"Creating test data")
a = Article(title="StackOverflow: 'Foo' not setting 'Bar'?")
pl = PreviewList(articles=[a])
session.add(a)
session.add(pl)
session.commit()
print(f"ArticlePreviewListAssociations: {session.query(ArticlePreviewListAssociation).all()}")
print(f"Deleting PreviewList")
session.delete(pl)
associations = session.query(ArticlePreviewListAssociation).all()
print(f"ArticlePreviewListAssociations: should be empty: {associations}")
if len(associations) > 0:
print("FAIL")
print("Reverting transaction")
session.rollback()
print("Deleting article")
session.delete(a)
articles_in_list = pl.articles
associations = session.query(ArticlePreviewListAssociation).all()
print(f"ArticlePreviewListAssociations: should not be empty: {associations}")
if len(associations) == 0:
print("FAIL")
print(f"Articles in PreviewList: should not be empty: {articles_in_list}")
if len(articles_in_list) == 0:
print("FAIL")
# desired outcome: pl.articles should be [None], not []
print("Reverting transaction")
session.rollback()
This may come down to "How can you make a many-to-many relationship where pk_A == 1 and pk_B == NULL include the None in A's list?"
The given examples would seem to assume that the order of related articles is preserved, even upon deletion. There are multiple approaches to that, for example the Ordering List extension, but it is easier to first solve the problem of preserving associations to deleted articles. This seems like a use case for an association object and proxy.
The Article class gets a new relationship so that deletions cascade in a session. The default ORM-level cascading behavior is to set the foreign key to NULL, but if the related association object is not loaded, we want to let the DB do it, so passive_deletes=True is used:
class Article(Base):
__tablename__ = 'articles'
id = Column(Integer, primary_key=True)
title = Column(String)
previewlist_associations = relationship(
'ArticlePreviewListAssociation', back_populates='article',
passive_deletes=True)
Instead of a many to many relationship PreviewList uses the association object pattern, along with an association proxy that replaces the many to many relationship. This time the cascades are a bit different, since the association object should be deleted, if the parent PreviewList is deleted:
class PreviewList(Base):
__tablename__ = 'preview_lists'
id = Column(Integer, primary_key=True)
article_associations = relationship(
'ArticlePreviewListAssociation', back_populates='preview_list',
cascade='all, delete-orphan', passive_deletes=True)
articles = association_proxy(
'article_associations', 'article',
creator=lambda a: ArticlePreviewListAssociation(article=a))
Originally the association object used previewlist_id as the primary key, but then a PreviewList could contain a single Article only. A surrogate key solves that. The foreign key configurations include the DB level cascades. These are the reason for using passive deletes:
class ArticlePreviewListAssociation(Base):
__tablename__ = 'associations'
id = Column(Integer, primary_key=True)
article_id = Column(
Integer, ForeignKey('articles.id', ondelete='SET NULL'))
previewlist_id = Column(
Integer, ForeignKey('preview_lists.id', ondelete='CASCADE'),
nullable=False)
# Using a unique constraint on a nullable column is a bit ugly, but
# at least this prevents inserting an Article multiple times to a
# PreviewList.
__table_args__ = (UniqueConstraint(article_id, previewlist_id), )
article = relationship(
'Article', back_populates='previewlist_associations')
preview_list = relationship(
'PreviewList', back_populates='article_associations')
With these changes in place no "FAIL" is printed.
I have a problem with SQL Alchemy, while trying to think about an SQL schema I encountered the following problem.
My schema is based on 2 classes, Flight and Trip.
A Trip includes 2 fields: flights_to and flights_from.
Any of the fields is basically a list of flights, it could be made of one flight, or many flights (Connection flights).
class Trip(Base):
__tablename__ = "Trip"
__table_args__ = {'sqlite_autoincrement': True}
id = Column(Integer, primary_key = True)
flights_to = relationship("Flight", backref="Trip")
flights_from = relationship("Flight", backref="Trip")
class Flight(Base):
__tablename__ = "Flight"
__table_args__ = {'sqlite_autoincrement': True}
id = Column(Integer, primary_key = True)
arrival_airport = Column(String(20))
departure_airport = Column(String(20))
flight_number = Column(Integer)
trip_id = Column(Integer, ForeignKey('Trip.id'))
The problem happens when I create 2 fields in the same type:
sqlalchemy.exc.ArgumentError: Error creating backref 'Trip' on relationship 'Trip.flights_from': property of that name exists on mapper 'Mapper|Flight|Flight'
I have thought about using 2 inheriting classes of types FlightTo and FlightFrom and saving them at two different tables, but what if I want to use a FlightFrom as a FlightTo? will the flight be duplicated in 2 tables?
I would appreciate your help.
backref is used to define a new property on the other class you are using relationship with. So you can't have two property which have the same name
You should rename your backref for the flights_from to any other name than Trip.
It will work then.
For Example:
class Person(Model):
id = Column(Integer, primary_key=True)
name = Column(String)
address = relationship("Address",backref="address")
class Address(Model):
id = Column(Integer, primary_key=True)
house_no = Column(Integer)
person_id = Column(Integer, ForeignKey('person.id'))
So you can access the person name with house_no 100 by:
query_address = Address.query.filter_by(house_no=100).first()
person = query_address.address
This returns you the person object.
Thus if you have multiple such names , it will give you an error
I'm a little confused over the use of the two modules from SQLAlchemy. This is the code I have:
Base = declarative_base()
class Restaurant(Base):
__tablename__ = 'restaurant'
id = Column(Integer, primary_key=True)
name = Column(String(250), nullable=False)
class MenuItem(Base):
__tablename__ = 'menu_item'
name =Column(String(80), nullable = False)
id = Column(Integer, primary_key = True)
description = Column(String(250))
price = Column(String(8))
course = Column(String(250))
restaurant_id = Column(Integer,ForeignKey('restaurant.id'))
restaurant = relationship(Restaurant)
I understand that ForeignKey is used to define the foreign key relationship between the restaurant_id column of menu_item table and the id column of restaurant table. But why then is restaurant = relationship(Restaurant) used?
restaurant_id will refer to an id (the column value). restaurant will refer to a Restaurant instance that will be lazy loaded from the db on access (or eager loaded if you set up the right stuff earlier). If you set backref on the relationship you can also access a list of MenuItem objects from a Restaurant.
Let's assume we have two tables in a many to many relationship as shown below:
class User(db.Model):
__tablename__ = 'user'
uid = db.Column(db.String(80), primary_key=True)
languages = db.relationship('Language', lazy='dynamic',
secondary='user_language')
class UserLanguage(db.Model):
__tablename__ = 'user_language'
__tableargs__ = (db.UniqueConstraint('uid', 'lid', name='user_language_ff'),)
id = db.Column(db.Integer, primary_key=True)
uid = db.Column(db.String(80), db.ForeignKey('user.uid'))
lid = db.Column(db.String(80), db.ForeignKey('language.lid'))
class Language(db.Model):
lid = db.Column(db.String(80), primary_key=True)
language_name = db.Column(db.String(30))
Now in the python shell:
In [4]: user = User.query.all()[0]
In [11]: user.languages = [Language('1', 'English')]
In [12]: db.session.commit()
In [13]: user2 = User.query.all()[1]
In [14]: user2.languages = [Language('1', 'English')]
In [15]: db.session.commit()
IntegrityError: (IntegrityError) column lid is not unique u'INSERT INTO language (lid, language_name) VALUES (?, ?)' ('1', 'English')
How can I let the relationship know that it should ignore duplicates and not break the unique constraint for the Language table? Of course, I could insert each language separately and check if the entry already exists in the table beforehand, but then much of the benefit offered by sqlalchemy relationships is gone.
The SQLAlchemy wiki has a collection of examples, one of which is how you might check uniqueness of instances.
The examples are a bit convoluted though. Basically, create a classmethod get_unique as an alternate constructor, which will first check a session cache, then try a query for existing instances, then finally create a new instance. Then call Language.get_unique(id, name) instead of Language(id, name).
I've written a more detailed answer in response to OP's bounty on another question.
I would suggest to read Association Proxy: Simplifying Association Objects. In this case your code would translate into something like below:
# NEW: need this function to auto-generate the PK for newly created Language
# here using uuid, but could be any generator
def _newid():
import uuid
return str(uuid.uuid4())
def _language_find_or_create(language_name):
language = Language.query.filter_by(language_name=language_name).first()
return language or Language(language_name=language_name)
class User(Base):
__tablename__ = 'user'
uid = Column(String(80), primary_key=True)
languages = relationship('Language', lazy='dynamic',
secondary='user_language')
# proxy the 'language_name' attribute from the 'languages' relationship
langs = association_proxy('languages', 'language_name',
creator=_language_find_or_create,
)
class UserLanguage(Base):
__tablename__ = 'user_language'
__tableargs__ = (UniqueConstraint('uid', 'lid', name='user_language_ff'),)
id = Column(Integer, primary_key=True)
uid = Column(String(80), ForeignKey('user.uid'))
lid = Column(String(80), ForeignKey('language.lid'))
class Language(Base):
__tablename__ = 'language'
# NEW: added a *default* here; replace with your implementation
lid = Column(String(80), primary_key=True, default=_newid)
language_name = Column(String(30))
# test code
user = User(uid="user-1")
# NEW: add languages using association_proxy property
user.langs.append("English")
user.langs.append("Spanish")
session.add(user)
session.commit()
user2 = User(uid="user-2")
user2.langs.append("English") # this will not create a new Language row...
user2.langs.append("German")
session.add(user2)
session.commit()