Inherit and add indexes from sqlalchemy mixins - python

Given a mixin with an index, how is it possible to add additional indexes to a model inheriting that mixin? With an index name like idx__TABLENAME__COLUMN the mixin is unable to obtain the TABLENAME for itself. If a __tablename__ is specified for the mixin, then duplicate index names arise.
Example code follows.
import sqlalchemy as sql
from sqlalchemy import Column, Index, String, Integer
from sqlalchemy.ext.declarative import declared_attr, declarative_base
Base = declarative_base()
class MixinOwner(object):
id = Column('id', Integer, primary_key=True)
owner = Column('owner', String)
#declared_attr
def __table_args__(cls):
return (Index('idx__%s__owner' % cls.__tablename__, 'owner'), )
class Letter(MixinOwner, Base):
__tablename__ = 'letter'
a = Column('a', String)
b = Column('b', String)
#declared_attr
def __table_args__(cls):
mixin_indexes = list(MixinOwner.__table_args__) # <--- Error (MixinOwner does not have attribute __tablename__)
mixin_indexes.extend([
Index('idx__letter__a', 'a'),
Index('idx__letter__b', 'b'),
])
return tuple(mixin_indexes)
class Word(MixinOwner, Base):
__tablename__ = 'word'
apple = Column('apple', String)
banana = Column('banana', String)
#declared_attr
def __table_args__(cls):
mixin_indexes = list(MixinOwner.__table_args__)
mixin_indexes.extend([
Index('idx__word__apple', 'apple'),
Index('idx__word__banana', 'banana'),
])
return tuple(mixin_indexes)
engine = sqlalchemy.create_engine('sqlite:///:memory:')
engine.connect()
Base.metadata.bind = engine
Base.metadata.create_all()
Session = sqlalchemy.orm.sessionmaker(bind=engine)
session = Session()

To incorporate indexes from mixins you need to use super to access the methods of base classes within the context of the subclass. In addition to this fix, which solves the original question I posed, there springs another question -- how to incorporate indexes from multiple mixins not just one (as was setup in the original question). The following code explains this larger issue as well. The solution is to iterate over the MRO and acquire the __table_args__ for each base class with the context of the model.
import sqlalchemy as sql
from sqlalchemy import Column, Index, String, Integer, Date
from sqlalchemy.ext.declarative import declared_attr, declarative_base
Base = declarative_base()
class MixinOwner(object):
id = Column('id', Integer, primary_key=True)
owner = Column('owner', String)
#declared_attr
def __table_args__(cls):
return (Index('idx__%s__owner' % cls.__tablename__, 'owner'), )
class MixinDate(object):
date = Column('date', Date)
#declared_attr
def __table_args__(cls):
return (Index('idx__%s__date' % cls.__tablename__, 'date'), )
# single mixin inheritance (original question) -- use super(cls, cls)
class Word(MixinOwner, Base):
__tablename__ = 'word'
apple = Column('apple', String)
banana = Column('banana', String)
#declared_attr
def __table_args__(cls):
mixin_indexes = list((super(cls, cls).__table_args__))
mixin_indexes.extend([
Index('idx__word__apple', 'apple'),
Index('idx__word__banana', 'banana'),
])
return tuple(mixin_indexes)
# multiple mixin iheritance (not in original question)
# iterate through __mro__ and aggregate __table_args__ from each base
class Letter(MixinOwner, MixinDate, Base):
__tablename__ = 'letter'
a = Column('a', String)
b = Column('b', String)
#declared_attr
def __table_args__(cls):
mixin_indexes = []
for base_class in cls.__mro__:
try:
mixin_indexes.extend(super(base_class, cls).__table_args__)
except AttributeError:
pass
mixin_indexes.extend([
Index('idx__letter__a', 'a'),
Index('idx__letter__b', 'b'),
])
return tuple(mixin_indexes)
engine = sql.create_engine('sqlite:///:memory:')
engine.connect()
Base.metadata.bind = engine
Base.metadata.create_all()
Session = sql.orm.sessionmaker(bind=engine)
session = Session()

Related

SQLAlchemy - Self-referential many-to-many relationship in custom declarative base class

I am building a Python application that uses SQLAlchemy, and I am trying to implement a many-to-many relationship between a custom declarative base class and itself (self-referential). But I cannot get it to work. I am attaching the code down below, as well as the error traceback, in case anyone can help :) All the entities of the model already extended from this base class, and the application was working so far, in case that helps.
Thanks!!
Code (non-functional):
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import MetaData, Table
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy import String
permissions = Table(
'permissions', MetaData(),
Column('origin_id', String, ForeignKey('bases.id'), primary_key=True),
Column('target_id', String, ForeignKey('bases.id'), primary_key=True)
)
class Base:
__tablename__ = 'bases'
__table_args__ = {
'mysql_engine': 'InnoDB'
}
id = Column(String, primary_key=True)
targets = relationship(
'Base',
secondary='permissions',
primaryjoin='Base.id == permissions.c.origin_id',
secondaryjoin='Base.id == permissions.c.target_id',
backref='origins'
# Reference:
# https://docs.sqlalchemy.org/en/14/orm/join_conditions.html#self-referential-many-to-many
)
Base = declarative_base(cls=Base)
Traceback:
class ContactMethod(Base):
File "/usr/local/lib/python3.8/site-packages/SQLAlchemy-1.4.23-py3.8-linux-x86_64.egg/sqlalchemy/orm/decl_api.py", line 72, in __init__
_as_declarative(reg, cls, dict_)
File "/usr/local/lib/python3.8/site-packages/SQLAlchemy-1.4.23-py3.8-linux-x86_64.egg/sqlalchemy/orm/decl_base.py", line 126, in _as_declarative
return _MapperConfig.setup_mapping(registry, cls, dict_, None, {})
File "/usr/local/lib/python3.8/site-packages/SQLAlchemy-1.4.23-py3.8-linux-x86_64.egg/sqlalchemy/orm/decl_base.py", line 177, in setup_mapping
return cfg_cls(registry, cls_, dict_, table, mapper_kw)
File "/usr/local/lib/python3.8/site-packages/SQLAlchemy-1.4.23-py3.8-linux-x86_64.egg/sqlalchemy/orm/decl_base.py", line 299, in __init__
self._scan_attributes()
File "/usr/local/lib/python3.8/site-packages/SQLAlchemy-1.4.23-py3.8-linux-x86_64.egg/sqlalchemy/orm/decl_base.py", line 511, in _scan_attributes
raise exc.InvalidRequestError(
sqlalchemy.exc.InvalidRequestError: Mapper properties (i.e. deferred,column_property(), relationship(), etc.) must be declared as #declared_attr callables on declarative mixin classes. For dataclass field() objects, use a lambda:
See a working self-containing example:
## Imports
from sqlalchemy import Column, ForeignKey, Integer, String, Table, create_engine
from sqlalchemy.orm import Session, as_declarative, declared_attr, relationship
## Configuration
engine = create_engine("sqlite:///:memory:", echo=True)
#as_declarative()
class Base(object):
#declared_attr
def __tablename__(cls):
return cls.__name__.lower()
permissions = Table(
"permissions",
Base.metadata,
Column("origin_id", String, ForeignKey("model_base.id"), primary_key=True),
Column("target_id", String, ForeignKey("model_base.id"), primary_key=True),
)
## Model definitions
class ModelBase(Base):
__tablename__ = "model_base"
id = Column(Integer, primary_key=True)
type = Column(String, nullable=False)
__mapper_args__ = {
"polymorphic_on": type,
"polymorphic_identity": None,
}
targets = relationship(
"ModelBase",
secondary="permissions",
primaryjoin="model_base.c.id == permissions.c.origin_id",
secondaryjoin="model_base.c.id == permissions.c.target_id",
backref="origins",
# lazy="raise",
)
class ClassA(ModelBase):
__tablename__ = "class_a"
__mapper_args__ = {"polymorphic_identity": "class_a"}
id = Column(ForeignKey("model_base.id"), primary_key=True)
name = Column(String)
class ClassB(ModelBase):
__tablename__ = "class_b"
__mapper_args__ = {"polymorphic_identity": "class_b"}
id = Column(ForeignKey("model_base.id"), primary_key=True)
name = Column(String)
value = Column(Integer)
## Tests
def _main():
with Session(engine) as session:
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
print("=" * 80)
# data
a1, a2 = [ClassA(name="a1"), ClassA(name="a2")]
b1, b2 = [ClassB(name="b1"), ClassB(name="b2", value=3)]
session.add_all([a1, a2, b1, b2])
session.flush()
a1.targets.append(a2)
a1.targets.append(b1)
a1.targets.append(b2)
print(b2.targets)
print(b2.origins)
session.commit()
session.expunge_all()
print("=" * 80)
a1 = session.query(ClassA).first()
print(a1)
print(a1.origins)
print(a1.targets)
session.commit()
if __name__ == "__main__":
_main()
Here’s an example of what the error output recommends to remediate:
from sqlalchemy.ext.declarative import declared_attr
class Base:
...
#declared_attr
def targets(cls):
return relationship(
'Base',
secondary='permissions',
primaryjoin='Base.id == permissions.c.origin_id',
secondaryjoin='Base.id == permissions.c.target_id',
backref='origins'
)
...
Sidenote: you can use the as_declarative mixin on your base class as a shortcut.
References
Augmenting the base: https://docs.sqlalchemy.org/en/13/orm/extensions/declarative/mixins.html#augmenting-the-base
declared_attr: https://docs.sqlalchemy.org/en/13/orm/extensions/declarative/api.html#sqlalchemy.ext.declarative.declared_attr
as_declarative: https://docs.sqlalchemy.org/en/13/orm/extensions/declarative/api.html#sqlalchemy.ext.declarative.as_declarative

Generating JSON from SQLAlchemy base class including subclases from relationship

I'm trying to generate the JSON of my SQLAlchemy classes, I followed this example:
https://blogs.gnome.org/danni/2013/03/07/generating-json-from-sqlalchemy-objects/
It’s working very fine, but now I want to include all the data of the subclasses generated by the relationship of SQLAchemy. I've tried several things, the last one is trying to iterate over the subclases but I don't know why the method subclasses doesn't return anything. This is the function tojson modified:
def tojson(self):
res=self.columnitems
for cls in self.__class__.__subclasses__():
res[cls.__name__]=cls.tojson()
return res
Do you know any way to do it?
Thanks in advance
I can't comment yet but based on the information provided I'm assuming you are trying to generate a json from your (related) sqlalchemy classes. You can use the marshmallow (https://marshmallow.readthedocs.io/en/latest/) for this.
The (quick) example below shows how you can generate a json using marshmallow of two related tables.
from sqlalchemy import Column, Integer, String, Boolean
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine, ForeignKey
from sqlalchemy.orm import sessionmaker, relationship
Base = declarative_base()
# Creating dummy classes...
class Owner(Base):
__tablename__ = 'owner'
id = Column('id', Integer, primary_key=True)
name = Column('name', String(250))
interested_in_cars = Column('interest', Boolean)
car = relationship('Car', uselist=False, back_populates="owner")
def __init__(self, name, interested_in_cars, id):
self.id = id
self.name = name
self.interested_in_cars = interested_in_cars
def __repr__(self):
return '< (id={id}) Owner: {name} - interested: {interested_in_cars} >'.format(id=self.id,
name=self.name,
interested_in_cars=self.interested_in_cars)
class Car(Base):
__tablename__ = 'car'
id = Column('id', Integer, primary_key=True)
brand = Column(String(250))
owner_id = Column(Integer, ForeignKey('owner.id'))
owner = relationship('Owner', back_populates='car')
def __init__(self, owner_id, brand):
self.owner_id = owner_id
self.brand = brand
def __repr__(self):
return '< Owner: {owner_id} - Car: {brand} >'.format(owner_id=self.owner_id, brand=self.brand)
engine = create_engine('sqlite:///')
session = sessionmaker()
session.configure(bind=engine)
ex_ses = session()
Base.metadata.create_all(engine)
owner_1 = Owner(interested_in_cars=True, name='Owner a', id=1)
owner_2 = Owner(interested_in_cars=False, name='Owner b', id=2)
ex_ses.add(owner_1)
ex_ses.add(owner_2)
# ID's - quick example
car_1 = Car(owner_id=1, brand='Car a')
car_2 = Car(owner_id=2, brand='Car b')
ex_ses.add(car_1)
ex_ses.add(car_2)
ex_ses.commit()
# Using marshmallow to generate the json
from marshmallow import Schema, fields, pprint
class OwnerShema(Schema):
id = fields.Int()
name = fields.String()
interested_in_cars = fields.Boolean()
car = fields.Nested('CarShema')
class CarShema(Schema):
id = fields.Int()
brand = fields.String()
# Example Owners and cars
owners_cars = ex_ses.query(Owner).all()
print('Owners and cars: ', owners_cars)
owners_cars_shema = OwnerShema()
pprint(owners_cars_shema.dump(owners_cars, many=True).data)
For more information see the marshmallow documentation (link provided above).

SQLAlchemy association proxy and lists

I'm trying to work out how to get an association proxy on a many to many relationship to return an empty list by default.
So based on the example at:
https://raw.githubusercontent.com/zzzeek/sqlalchemy/master/examples/generic_associations/discriminator_on_association.py
We have:
from sqlalchemy.ext.declarative import as_declarative, declared_attr
from sqlalchemy import create_engine, Integer, Column, \
String, ForeignKey
from sqlalchemy.orm import Session, relationship, backref
from sqlalchemy.ext.associationproxy import association_proxy
#as_declarative()
class Base(object):
"""Base class which provides automated table name
and surrogate primary key column.
"""
#declared_attr
def __tablename__(cls):
return cls.__name__.lower()
id = Column(Integer, primary_key=True)
class AddressAssociation(Base):
"""Associates a collection of Address objects
with a particular parent.
"""
__tablename__ = "address_association"
discriminator = Column(String)
"""Refers to the type of parent."""
__mapper_args__ = {"polymorphic_on": discriminator}
class Address(Base):
"""The Address class.
This represents all address records in a
single table.
"""
association_id = Column(Integer, ForeignKey("address_association.id"))
street = Column(String)
city = Column(String)
zip = Column(String)
association = relationship("AddressAssociation", backref="addresses")
parent = association_proxy("association", "parent")
def __repr__(self):
return "%s(street=%r, city=%r, zip=%r)" % \
(self.__class__.__name__, self.street,
self.city, self.zip)
class HasAddresses(object):
"""HasAddresses mixin, creates a relationship to
the address_association table for each parent.
"""
#declared_attr
def address_association_id(cls):
return Column(Integer, ForeignKey("address_association.id"))
#declared_attr
def address_association(cls):
name = cls.__name__
discriminator = name.lower()
assoc_cls = type(
"%sAddressAssociation" % name,
(AddressAssociation, ),
dict(
__tablename__=None,
__mapper_args__={
"polymorphic_identity": discriminator
}
)
)
cls.addresses = association_proxy(
"address_association", "addresses",
creator=lambda addresses: assoc_cls(addresses=addresses)
)
return relationship(assoc_cls,
backref=backref("parent", uselist=False))
class Customer(HasAddresses, Base):
name = Column(String)
class Supplier(HasAddresses, Base):
company_name = Column(String)
engine = create_engine('sqlite://', echo=True)
Base.metadata.create_all(engine)
session = Session(engine)
But if I try to look at the address of a Customer, I get None rather than [] that I'd expect:
(Pdb) foo = Customer()
(Pdb) foo.addresses
(Pdb)
(Pdb) type(foo.addresses)
<type 'NoneType'>
Which means I can't simply iterate over foo.addresses without first checking it is not None.
I tried setting uselist = True on the association proxy:
return relationship(assoc_cls,
uselist=True,
backref=backref("parent", uselist=False))
Which sort of works, but then the constructor fails:
(Pdb) foo.addresses
[]
(Pdb) foo.addresses.append(Address())
*** TypeError: Incompatible collection type: Address is not list-like
(Pdb)
I tried messing with the creator to make it construct a list, but then cause all manner of side effects, included nested lists:
cls.addresses = association_proxy(
"address_association", "addresses",
creator=lambda addresses: assoc_cls(addresses=[addresses,])
Any idea the best way to sort this out properly?
-Matt

Automating sqlalchemy declarative model creation

Let’s say we have several sqlalchemy models for catalogues:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer
from sqlalchemy.orm import relationship
Base = declarative_base()
class Plane(Base):
__tablename__ = 'Plane'
plane_id = Column(Integer, primary_key=True)
class Car(Base):
__tablename__ = 'Car'
car_id = Column(Integer, primary_key=True)
Now for import/export purposes we want to relate these to external ids. So for Plane we would write:
class PlaneID(Base):
issuer = Column(String(32), primary_key=True)
external_id = Column(String(16), primary_key=True)
plane_id = Column(Integer, ForeignKey(Plane.plane_id))
plane = relationship(Plane, backref='external_ids')
A CarID model would be defined in exactly the same way.
What are possibilities to automate this process?
Maybe we could use a mixin, factory, decorator or meta class. How would we generate the dynamically named Columns then? It would be good to be able to add more Columns to the generated models as needed. For example:
class CarID(ExternalID):
valid_from = Column(Date)
You can subclass DeclarativeMeta - the metaclass used in declarative_base function:
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
class ExternalObject(DeclarativeMeta):
def __new__(mcs, name, bases, attributes):
if 'issuer' not in attributes:
attributes['issuer'] = Column(String(32), primary_key=True)
if 'external_id' not in attributes:
attributes['external_id'] = Column(String(16), primary_key=True)
if name[-2:] == 'ID':
ext_cls_name = name[:-2]
attr_rel = ext_cls_name.lower()
attr_id = '%s_id' % attr_rel
if attr_rel in attributes or attr_id in attributes:
# Some code here in case 'car' or 'car_id' attribute is defined in new class
pass
attributes[attr_id] = Column(Integer, ForeignKey('%s.%s' % (ext_cls_name, attr_id)))
attributes[attr_rel] = relationship(ext_cls_name, backref='external_ids')
new_cls = super().__new__(mcs, name, bases, attributes)
return new_cls
ExternalID = declarative_base(metaclass=ExternalObject)
After that you can create subclass from ExternalID and add another attributes like you did for CarID.

sqlalchemy: column_prefix causes issues accessing model attributes

I went searching w/o result in a way to get the integer value or the boolean value from an object model created via sqlalchemy,
I mean i can add it and it works flawless but i cant get the integer value or the boolean value all i get when i tried to print it is the object name:
from sqlalchemy import create_engine, MetaData, Table, Column,Integer,String,Boolean,Sequence
from sqlalchemy.orm import mapper, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
import json
class Bookmarks(object):
pass
#----------------------------------------------------------------------
engine = create_engine('postgresql://u:p#localghost/asd', echo=True)
Base = declarative_base()
class Tramo(Base):
__tablename__ = 'tramos'
__mapper_args__ = {'column_prefix':'tramos'}
id = Column(Integer, primary_key=True)
nombre = Column(String)
tramo_data = Column(String)
estado = Column(Boolean,default=True)
def __init__(self,nombre,tramo_data):
self.nombre=nombre
self.tramo_data=tramo_data
def __repr__(self):
return "[id:%s][nombre:%s][tramo:%s]" % (getattr(self, 'id'), self.nombre,self.tramo_data)
Session = sessionmaker(bind=engine)
session = Session()
tabla = Tramo.__table__
metadata = Base.metadata
metadata.create_all(engine)
b=Tramo('tramo1','adadas')
session.add(b)
session.commit()
print b
print b.id
its prints
[id:tramos.id][nombre:tramo1][tramo:adadas]
tramos.id
i cant get to print the id value, looks like the object column is in there but it doesn't return the value ot the property
i even use
session.refresh(b)
after the add but the result is the same.
According to the documentation Naming All Columns with a Prefix:
...prefix to the mapped attribute names relative to the
(table) column name ...
Since you define the mapped attributes in your class, I do not think it does what you desire.
Solution-1: remove the 'column_prefix':'tramos' from your __mapper_args__
Solution-2: print b.tramosid will print its id. You would need to change the __repr__ accordingly:
def __repr__(self):
return "[id:%s][nombre:%s][tramo:%s]" % (getattr(self, 'tramosid'), self.nombre, self.tramo_data)

Categories