Accessing sqlalchemy Foreign Key relationship information programmatically - python

I'm trying to build tests for flask-sqlalchemy models to confirm they match the tables in the database. Using reflection, I can get the db tables, columns, and foreign keys. With the models I know how to check tablename and column names, but I can't figure out how to see the defined foreign key relationships programatically.
Model example:
class Example(db.Model):
__table_args__ = {'schema': 'defined_schema'}
__tablename__ = 'example'
id = db.Column(db.BigInteger, primary_key=True)
name = db.Column(db.String)
info = db.Column(db.BigInteger, db.ForeignKey(Info.id))
descr = db.Column(db.String)
Given a Model m, I've tried:
dir(m)
m.__dict__

Using sqlalchemy.inspect(model).columns.foreign_keys and a bit of string manipulation, I was able to compare model foreign key relationships to those in the DB

Related

Flask SQLAlchemy could not find table with which to generate Foreign Key even though table exists

class BigBoxModel(db.Model):
__tablename__ = "bigbox"
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
name = db.Column(db.String, unique=True, nullable=False)
vendor_id = db.Column(db.BigInteger, db.ForeignKey("vendors.id"), nullable=False)
I am trying to work with the bigbox table but keep getting the error:
Foreign key associated with column 'bigbox.vendor_id' could not find table 'vendors' with which to generate a foreign key to target column 'id'
The vendors table exists in another file. I cannot import it because that results in circular imports as there are a lot of commonly used libraries present in both files. I have also checked all the table names by running the command:
db.engine.table_names()
And the vendors table does exist but I still keep getting the same error. I am using Flask, SQLalchemy, Postgres.
You could try to use the Class name for foreign key mapping. In your BigBoxModel, you can have something like
vendor_id = db.Column(db.BigInteger, db.ForeignKey(Vendors.id), nullable=False). In this case Vendors is assumed to be the class name.

Python & SQLAlchemy many-to-many relation error after specify schema name

Here the code:
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
AvailableModules = db.Table('available_modules',
db.Column('customer_id', db.Integer, db.ForeignKey('customers.id')),
db.Column('module_id', db.Integer, db.ForeignKey('modules.id')),
schema='tcloud_admin')
class Customer(db.Model):
__tablename__ = 'customers'
__table_args__ = {"schema":"tcloud_admin"}
id = db.Column(db.Integer, primary_key=True)
....
modules = db.relationship('Module', secondary=AvailableModules)
class Module(db.Model):
__tablename__ = 'modules'
__table_args__ = {"schema":"tcloud_admin"}
id = db.Column(db.Integer, primary_key=True)
....
Trying the above script gives me this error:
sqlalchemy.exc.NoReferencedTableError: Foreign key associated with
column 'available_modules.customer_id' could not find table
'customers' with which to generate a foreign key to target column 'id'
Without specifying the schema name everything works fine.
When using a custom schema name all foreign keys that reference a table or column in that schema must explicitly contain that schema name too.
Your ForeignKey('customers.id') references a different table customers which is not inside your custom schema. As this table does not exist at all you get the corresponding error.
To fix the error change the foreign keys:
db.ForeignKey('tcloud_admin.customers.id')
db.ForeignKey('tcloud_admin.modules.id')
This is also explained in the docs

Properly cascade delete with sqlalchemy association proxy

I have a self-referential relationship in sqlalchemy that is based heavily on the example found in this answer.
I have a table of users, and an association table that links a primary user to a secondary user. User A can be primary for user B, and B may or may not also be a primary for user A. It works exactly like the twitter analogy in the answer I linked above.
This works fine, except that I don't know how to establish cascade rules for an association proxy. Currently, if I delete a user, the association record remains, but it nulls out any FKs to the deleted user. I would like the delete to cascade to the association table and remove the record.
I also need to be able to disassociate users, which would only remove the association record, but would propagate to the "is_primary_of" and "is_secondary_of" association proxies of the users.
Can anyone help me figure out how to integrate these behaviors into the models that I have? Code is below. Thanks!
import sqlalchemy
import sqlalchemy.orm
import sqlalchemy.ext.declarative
import sqlalchemy.ext.associationproxy
# This is the base class from which all sqlalchemy table objects must inherit
SAModelBase = sqlalchemy.ext.declarative.declarative_base()
class UserAssociation(SAModelBase):
__tablename__ = 'user_associations'
# Columns
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
# Foreign key columns
primary_user_id = sqlalchemy.Column(sqlalchemy.Integer,
sqlalchemy.ForeignKey('users.id', name='user_association_primary_user_fk'))
secondary_user_id = sqlalchemy.Column(sqlalchemy.Integer,
sqlalchemy.ForeignKey('users.id', name='user_association_secondary_user_fk'))
# Foreign key relationships
primary_user = sqlalchemy.orm.relationship('User',
foreign_keys=primary_user_id,
backref='secondary_users')
secondary_user = sqlalchemy.orm.relationship('User',
foreign_keys=secondary_user_id,
backref='primary_users')
def __init__(self, primary, secondary, **kwargs):
self.primary_user = primary
self.secondary_user = secondary
for kw,arg in kwargs.items():
setattr(self, kw, arg)
class User(SAModelBase):
__tablename__ = 'users'
# Columns
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
first_name = sqlalchemy.Column(sqlalchemy.String)
last_name = sqlalchemy.Column(sqlalchemy.String)
is_primary_of = sqlalchemy.ext.associationproxy.association_proxy('secondary_users', 'secondary_user')
is_secondary_of = sqlalchemy.ext.associationproxy.association_proxy('primary_users', 'primary_user')
def associate(self, user, **kwargs):
UserAssociation(primary=self, secondary=user, **kwargs)
Turns out to be pretty straightforward. The backrefs in the original code were just strings, but they can instead be backref objects. This allows you to set cascade behavior. See the sqlalchemy documentation on backref arguments.
The only changes required here are in the UserAssociation object. It now reads:
# Foreign key columns
primary_user_id = sqlalchemy.Column(sqlalchemy.Integer,
sqlalchemy.ForeignKey('users.id',
name='user_association_primary_user_fk'),
nullable=False)
secondary_user_id = sqlalchemy.Column(sqlalchemy.Integer,
sqlalchemy.ForeignKey('users.id',
name='user_association_associated_user_fk'),
nullable=False)
# Foreign key relationships
primary_user = sqlalchemy.orm.relationship('User',
foreign_keys=primary_user_id,
backref=sqlalchemy.orm.backref('secondary_users',
cascade='all, delete-orphan'))
secondary_user = sqlalchemy.orm.relationship('User',
foreign_keys=secondary_user_id,
backref=sqlalchemy.orm.backref('primary_users',
cascade='all, delete-orphan'))
The backref keyword argument is now a backref object instead of a string. I was also able to make the foreign key columns non-nullable, since it now cascades deleted users such that the associations are deleted as well.

How to create a field with a list of foreign keys in SQLAlchemy?

I am trying to store a list of models within the field of another model. Here is a trivial example below, where I have an existing model, Actor, and I want to create a new model, Movie, with the field Movie.list_of_actors:
import uuid
from sqlalchemy import Boolean, Column, Integer, String, DateTime
from sqlalchemy.schema import ForeignKey
rom sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class Actor(Base):
__tablename__ = 'actors'
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String)
nickname = Column(String)
academy_awards = Column(Integer)
# This is my new model:
class Movie(Base):
__tablename__ = 'movies'
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
title = Column(String)
# How do I make this a list of foreign keys???
list_of_actors = Column(UUID(as_uuid=True), ForeignKey('actors.id'))
I understand that this can be done with a many-to-many relationship, but is there a more simple solution? Note that I don't need to look up which Movie's an Actor is in - I just want to create a new Movie model and access the list of my Actor's. And ideally, I would prefer not to add any new fields to my Actor model.
I've gone through the tutorials using the relationships API, which outlines the various one-to-many/many-to-many combinations using back_propagates and backref here: http://docs.sqlalchemy.org/en/latest/orm/basic_relationships.html But I can't seem to implement my list of foreign keys without creating a full-blown many-to-many implementation.
But if a many-to-many implementation is the only way to proceed, is there a way to implement it without having to create an "association table"? The "association table" is described here: http://docs.sqlalchemy.org/en/latest/orm/basic_relationships.html#many-to-many ? Either way, an example would be very helpful!
Also, if it matters, I am using Postgres 9.5. I see from this post there might be support for arrays in Postgres, so any thoughts on that could be helpful.
Update
It looks like the only reasonable approach here is to create an association table, as shown in the selected answer below. I tried using ARRAY from SQLAlchemy's Postgres Dialect but it doesn't seem to support Foreign Keys. In my example above, I used the following column:
list_of_actors = Column('actors', postgresql.ARRAY(ForeignKey('actors.id')))
but it gives me an error. It seems like support for Postgres ARRAY with Foreign Keys is in progress, but still isn't quite there. Here is the most up to date source of information that I found: http://blog.2ndquadrant.com/postgresql-9-3-development-array-element-foreign-keys/
If you want many actors to be associated to a movie, and many movies be associated to an actor, you want a many-to-many. This means you need an association table. Otherwise, you could chuck away normalisation and use a NoSQL database.
An association table solution might resemble:
class Actor(Base):
__tablename__ = 'actors'
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String)
nickname = Column(String)
academy_awards = Column(Integer)
class Movie(Base):
__tablename__ = 'movies'
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
title = Column(String)
actors = relationship('ActorMovie', uselist=True, backref='movies')
class ActorMovie(Base):
__tablename__ = 'actor_movies'
actor_id = Column(UUID(as_uuid=True), ForeignKey('actors.id'))
movie_id = Column(UUID(as_uuid=True), ForeignKey('movies.id'))
If you don't want ActorMovie to be an object inheriting from Base, you could use sqlachlemy.schema.Table.

How to create viewonly field in SqlAlchemy and GeoAlchemy2?

I am trying to dynamically populate a field using SqlAlchemy 0.8.4 and GeoAlchemy2 0.2.2. The goal is to assign a District to the Facility based on the facility's position when it is read from the database. The code looks like this:
class District(Base):
__tablename__ = 'districts'
id = Column(Integer, primary_key=True)
geom = Column('geog', Geometry(geometry_type='POLYGON'), nullable=False)
class Facility(Base):
__tablename__ = 'facilities'
id = Column(Integer, primary_key=True)
pos = Column('geog', Geometry(geometry_type='POINT'), nullable=True)
district = relationship(District,
viewonly=True,
primaryjoin="District.geom.ST_Contains(Facility.pos)",
foreign_keys=[District.id])
But this gives me the following error:
ArgumentError: Could not locate any relevant foreign key columns for
primary join condition 'ST_Contains(districts.geog, facilities.geog)'
on relationship Facility.district. Ensure that referencing columns
are associated with a ForeignKey or ForeignKeyConstraint, or are
annotated in the join condition with the foreign() annotation
I would really like to avoid having foreignkey relation between these classes since the districts are constantly changing, but would like to get a facility with a district set without querying the database again.
How should I solve this?

Categories