How do I *dynamically* set the schema in SQLAlchemy for MSSQL? - python

On this question I learned how to set the schema on an ORM definition:
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Notification(Base):
__tablename__ = "dog"
__table_args__ = {"schema": "animal"}
id = Column(Integer, primary_key=True)
name = Column(String)
But I now need to make the schema configurable. I have tried passing the table_args parameter at object creation, but it's still trying the schema I put on the class definition.

The better solution I have found so far is to create a function that returns the class:
function get_notification_class(schema: str):
class Notification(Base):
__tablename__ = "dog"
__table_args__ = {"schema": schema}
id = Column(Integer, primary_key=True)
name = Column(String)
return Notification
And then, to use it
Notification = get_notification_class('animal')
obj = Notification('1', 'doggy')

Related

SQLAlchemy declared_attr on Column with Foreign Key

I have a setup where I am using an abstract Machine base class which has several subclasses for different type of machines. The reasoning is that I want to use different database schemas for those machines, which have some tables that are similar but some that are entirely different.
class Machine(Base):
__abstract__ = True
__tablename__ = 'machines'
id = Column('id', BigInteger, nullable=False, primary_key=True)
class MachineA(Machine):
system = relationship('SystemA')
__table_args__ = {'schema': 'A'}
class MachineB(Machine):
system = relationship('SystemB')
__table_args__ = {'schema': 'B'}
So far this is working as intended, but as soon as I start adding tables with foreign keys for the individual schemas, I am running into an issue.
Here is an example for a system table:
class System(Base):
__tablename__ = 'system'
__abstract__ = True
id = Column('id', BigInteger, primary_key=True, autoincrement=True)
machine_id = Column(BigInteger, ForeignKey('machines.id'), nullable=False)
ts = Column('ts', TIMESTAMP(timezone=True), nullable=False)
value = Column('value', JSONB())
__table_args__ = (UniqueConstraint('machine_id', ts, value),
Index('system_machine_id_index', machine_id),
Index('system_ts_index', ts))
class SystemA(System):
__table_args__ = (*System.__table_args__,
{'schema': 'A'})
class SystemB(System):
__table_args__ = (*System.__table_args__,
{'schema': 'B'})
When creating the tables, the following error is raised:
sqlalchemy.exc.InvalidRequestError: Columns with foreign keys to other columns must be declared as #declared_attr callables on declarative mixin classes.
For dataclass field() objects, use a lambda:.
I tried using:
#declared_attr
def machine_id(cls):
return Column(BigInteger, ForeignKey('machines.id'), nullable=False)
which didn't work. Am I missing something? Thanks!
Found the solution. I had to make machine_id a string:
__table_args__ = (UniqueConstraint('machine_id', ts, value),
Index('system_machine_id_index', 'machine_id'),
Index('system_ts_index', ts))
and more importantly, reference the schema in the ForeignKey call:
#declared_attr
def machine_id(cls):
return Column(BigInteger, ForeignKey('A.machines.id'), nullable=False)
Specifying the schema in the __table_args__ is not enough for the mixin declaration.
you can try this
#declarative_mixin
class System(Base):
...
#declared_attr
def machine_id(cls):
...

sqlalchemy: create two databases in the same file

I'm trying to create two separate databases in a single file using SQLAlchemy. Here's the code I have so far:
from sqlalchemy import create_engine, Column, String, Integer, inspect
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Table1(Base):
__tablename__ = 'table_1'
id = Column(Integer, primary_key=True)
name = Column(String)
class Table2(Base):
__tablename__ = 'table_2'
id = Column(Integer, primary_key=True)
name = Column(String)
engine1 = create_engine('sqlite:///db1.db')
engine2 = create_engine('sqlite:///db2.db')
Base.metadata.drop_all(bind=engine1)
Base.metadata.drop_all(bind=engine2)
Base.metadata.create_all(bind=engine1)
Base.metadata.create_all(bind=engine2)
print(inspect(engine1).get_table_names())
# ['table_1', 'table_2']
print(inspect(engine2).get_table_names())
# ['table_1', 'table_2']
I want to create only Table1 in db1 and only Table2 in db2; however, I'm getting both tables in both databases.
Is there anyway to resolve this or do I need to create the databases in two separate files.
Your problem isn't caused by trying to create the two databases in the same module. Rather, you are calling create_all() on the same metadata object that has mapped both of the tables. E.g.
print(Base.metadata.tables)
result:
dict_keys(['table_1', 'table_2'])
From the docs about MetaData.create_all():
This method will issue queries that first check for the existence of
each individual table, and if not found will issue the CREATE
statements...
Key point is it checks for the existence of each table. So here:
Base.metadata.create_all(bind=engine1)
Base.metadata.create_all(bind=engine2)
... it first checks for both tables in the db referenced by engine1, doesn't find them and creates them. Then, it checks for both tables in the db referenced by engine2, doesn't find them, and creates them.
There's a couple of options.
Have different Base objects (i.e. a distinct MetaData instance) for each database:
Base1 = declarative_base()
Base2 = declarative_base()
class Table1(Base1):
__tablename__ = 'table_1'
id = Column(Integer, primary_key=True)
name = Column(String)
class Table2(Base2):
__tablename__ = 'table_2'
id = Column(Integer, primary_key=True)
name = Column(String)
engine1 = create_engine('sqlite:///db1.db')
engine2 = create_engine('sqlite:///db2.db')
Base1.metadata.drop_all(bind=engine1)
Base2.metadata.drop_all(bind=engine2)
Base1.metadata.create_all(bind=engine1)
Base2.metadata.create_all(bind=engine2)
print(inspect(engine1).get_table_names())
# ['table_1']
print(inspect(engine2).get_table_names())
# ['table_2']
Or, selectively create the tables while binding to the desired engine:
Base = declarative_base()
class Table1(Base):
__tablename__ = 'table_1'
id = Column(Integer, primary_key=True)
name = Column(String)
class Table2(Base):
__tablename__ = 'table_2'
id = Column(Integer, primary_key=True)
name = Column(String)
engine1 = create_engine('sqlite:///db1.db')
engine2 = create_engine('sqlite:///db2.db')
Base.metadata.drop_all(bind=engine1)
Base.metadata.drop_all(bind=engine2)
Base.metadata.tables['table_1'].create(bind=engine1)
Base.metadata.tables['table_2'].create(bind=engine2)
print(inspect(engine1).get_table_names())
# ['table_1']
print(inspect(engine2).get_table_names())
# ['table_2']

sqlalchemy foreignkey relation with parent class

I have the following database schema:
Parent table:
id - primary key identifier.
type - polymorphic_identity.
name - string data column.
Child table - inherits Parent:
id - primary key identifier.
parent_id - foreignkey to Parent.
category - string data column.
Summing up I have two tables. Table Child inherits from Parent and also have a foreignkey to it.
UPD: I really need both inheritance and foreignkey. This example is only a short demo which reproduces the problem.
I used declarative_base to declare the schema:
# -*- coding: utf-8 -*-
from sqlalchemy import Column, String, Integer, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class Parent(Base):
__tablename__ = "Parent"
id = Column(Integer, primary_key=True)
type = Column(String(250))
name = Column(String(250))
__mapper_args__ = {
'polymorphic_identity':'Parent',
'polymorphic_on':type
}
class Child(Parent):
__tablename__ = 'Child'
id = Column(Integer, ForeignKey('Parent.id'), primary_key=True)
parent_id = Column(ForeignKey("Parent.id"), nullable=True)
category = Column(String(250))
__mapper_args__ = {
'polymorphic_identity':'Child',
}
engine = create_engine('postgresql+psycopg2://joe:joe#localhost/alch')
session = sessionmaker()
session.configure(bind=engine)
Base.metadata.create_all(engine)
But when I run the code I get the following error:
sqlalchemy.exc.AmbiguousForeignKeysError: Can't determine join between 'Parent' and 'Child'; tables have more than one foreign key constraint relationship between them. Please specify the 'onclause' of this join explicitly.
I have tried to set relationship attribute myself for Parent or for Child separately and for both too. Tried to use primaryjoin and foreign_keys parameters of relationship. But the error was the same.
I'm totally confused about this error.
I need help.
I found the solution here.
SQLAlchemy needs a hint in this situation: a inherit_condition field in Child's __mapper_args__ does the trick.
# -*- coding: utf-8 -*-
from sqlalchemy import Column, String, Integer, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class Parent(Base):
__tablename__ = "Parent"
id = Column(Integer, primary_key=True)
type = Column(String(250))
name = Column(String(250))
__mapper_args__ = {
'polymorphic_identity':'Parent',
'polymorphic_on':type
}
class Child(Parent):
__tablename__ = 'Child'
id = Column(Integer, ForeignKey('Parent.id'), primary_key=True)
parent_id = Column(ForeignKey("Parent.id"), nullable=True)
category = Column(String(250))
parent = relationship(Parent, foreign_keys=[parent_id])
__mapper_args__ = {
'polymorphic_identity':'Child',
'inherit_condition': id == Parent.id,
}
engine = create_engine('postgresql+psycopg2://joe:joe#localhost/alch')
session = sessionmaker()
session.configure(bind=engine)
Base.metadata.create_all(engine)
Have you tried removing the Foreign Key for the Child id field?
id = Column(Integer, ForeignKey('Parent.id'), primary_key=True)
parent_id = Column(ForeignKey("Parent.id"), nullable=True)
You need something like this:
id = Column(Integer, auto_increment=True, primary_key=True)
parent_id = Column(ForeignKey("Parent.id"), nullable=True)

Problems with Update and Table Inheritance with SQLAlchemy

I'm building an inheritance table schema like the following:
Spec Code
class Person(Base):
__tablename__ = 'people'
id = Column(Integer, primary_key=True)
discriminator = Column('type', String(50))
updated = Column(DateTime, server_default=func.now(), onupdate=func.now())
name = Column(String(50))
__mapper_args__ = {'polymorphic_on': discriminator}
class Engineer(Person):
__mapper_args__ = {'polymorphic_identity': 'engineer'}
start_date = Column(DateTime)
class Manager(Person):
__mapper_args__ = {'polymorphic_identity': 'manager'}
start_date = Column(DateTime)
UPDATED (WORKING) CODE
import os
import sys
from sqlalchemy import Column, create_engine, ForeignKey, Integer, String, DateTime
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql import func
from sqlalchemy.ext.declarative import declarative_base
try:
os.remove('test.db')
except FileNotFoundError:
pass
engine = create_engine('sqlite:///test.db', echo=True)
Session = sessionmaker(engine)
Base = declarative_base()
class People(Base):
__tablename__ = 'people'
discriminator = Column('type', String(50))
__mapper_args__ = {'polymorphic_on': discriminator}
id = Column(Integer, primary_key=True)
name = Column(String(50))
updated = Column(DateTime, server_default=func.now(), onupdate=func.now())
class Engineer(People):
__tablename__ = 'engineer'
__mapper_args__ = {'polymorphic_identity': 'engineer'}
id = Column(Integer, ForeignKey('people.id'), primary_key=True)
kind = Column(String(100), nullable=True)
Base.metadata.create_all(engine)
session = Session()
e = Engineer()
e.name = 'Mike'
session.add(e)
session.flush()
session.commit()
# works when updating the object
e.name = "Doug"
session.add(e)
session.commit()
# works using the base class for the query
count = session.query(People).filter(
People.name.is_('Doug')).update({People.name: 'James'})
# fails when using the derived class
count = session.query(Engineer).filter(
Engineer.name.is_('James')).update({Engineer.name: 'Mary'})
session.commit()
print("Count: {}".format(count))
Note: this is slightly modified example from sql docs
If I try to update the name for Engineer two things should happen.
update statement to the People table on column name
automatic trigger of update to the updated column on the People table
For now, i'd like to focus on number 1. Things like the example below (as also documented in the full code example) will result in invalid SQL
session.query(Engineer).filter(
Engineer.name.is_('James')).update({Engineer.name: 'Mary'})
I believe the above generates the following:
UPDATE engineer SET name=?, updated=CURRENT_TIMESTAMP FROM people WHERE people.name IS ?
Again, this is invalid. The statement is trying to update rows in incorrect table. name is in the base table.
I'm a little unclear about how inheritance tables should work but it seems like updates should work transparently with the derived object. Meaning, when I update Engineer.name querying against the Engineer object SQLAlchemy should know to update the People table. To complicate things a bit more, what happens if I try to update columns which exist in two tables
session.query(Engineer).filter(
Engineer.name.is_('James')).update({Engineer.name: 'Mary', Engineer.start_date: '1997-01-01'})
I suspect SQLAlchemy will not issue two update statements.

Unable to create self referencing foreign key in flask-sqlalchemy

I have a model Region and each Region can have sub-regions. Each sub-region has a field parent_id which is the id of its parent region. Here is how my model looks like
class Region(db.Model):
__tablename__ = 'regions'
__table_args__ = {'schema': 'schema_name'}
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
parent_id = db.Column(db.Integer, db.ForeignKey('regions.id'))
parent = db.relationship('Region', primaryjoin=('Region.parent_id==Region.id'), backref='sub-regions')
created_at = db.Column(db.DateTime, default=db.func.now())
deleted_at = db.Column(db.DateTime)
Bu when i try to do db.create_all i get this error sqlalchemy.exc.NoReferencedTableError: Foreign key associated with column 'regions.parent_id' could not find table 'regions' with which to generate a foreign key to target column 'id'
Why cant it find regions when i am specifying it in __tablename__? I am using flask-sqlalchemy version 1.0
EDIT --
i removed the line
__table_args__ = {'schema': 'schema_name'}
from my code and it works. Beats the hell out of me.
You must tell SQLAlchemy what the "remote side" of the relationship is, to distinguish between the current row and the row being joined to. The relevant explanation is located partway into this section of the documentation on relationships.
The relationship might look like this:
parent = db.relationship('Region', remote_side=id, backref='sub_regions')
This is an example demonstrating a self-referential relationship:
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.engine import create_engine
from sqlalchemy.ext.declarative.api import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
engine = create_engine('sqlite:///:memory:', echo=True)
Session = sessionmaker(engine)
Base = declarative_base(engine)
session = Session()
class Region(Base):
__tablename__ = 'region'
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
parent_id = Column(Integer, ForeignKey('region.id'), index=True)
parent = relationship(lambda: Region, remote_side=id, backref='sub_regions')
Base.metadata.create_all()
r1 = Region(name='United States of America')
r2 = Region(name='California', parent=r1)
session.add_all((r1, r2))
session.commit()
ca = session.query(Region).filter_by(name='California').first()
print ca.parent.name
There will be many lines of SQL output since echo is on, and the script will print 'United States of America' at the end.
I had the same issue with the schema name argument. What I changed to get it to work was to reference the table class directly in ForeignKey and relationships instead of using a string.
Example:
parent_id = Column(Integer, ForeignKey(Region.id), index=True)
parent = relationship(lambda: Region, remote_side=id, backref='sub_regions')
If you use a schema for any table, other tables that have foreign keys referencing those schema tables must provide the name of the schema. See the docs here
class Table(db.Model):
__tablename__ = 'table_1_name'
__table_args__ = {'schema': 'my_schema'}
id = Column('id', Integer, primary_key=True)
...
class AnotherTable(db.Model):
__tablename__ = 'table_2_name'
# Doesn't matter if this belongs to the same or different schema
# __table_args__ = {'schema': 'my_schema'}
id = Column('id', Integer, primary_key=True)
t1_id = Column(Integer, ForeignKey('my_schema.table_1_name.id'))
...
Works for both SQLAlchemy and Flask-SQLAlchemy. Hope this helps. :D
I only see slight differences from #davidism, but here's what works for me in straight SQLAlchemy.
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.orm import backref
class Region(Base):
__tablename__ = 'region'
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey('region.id'), index=True)
sub_regions = relationship('Region', backref=backref('parent', remote_side='Region.id'))
As he points out I'm guessing you wont need the imports, but should prefix them with db, so something like:
class Region(db.Model):
__tablename__ = 'region'
id = db.Column(db.Integer, primary_key=True)
parent_id = db.Column(db.Integer, db.ForeignKey('region.id'), index=True)
sub_regions = db.relationship('Region', backref=db.backref('parent', remote_side='Region.id'))

Categories

Resources