Is anyone familiar with ActiveRecord's "has_many :through" relations for models? I'm not really a Rails guy, but that's basically what I'm trying to do.
As a contrived example consider Projects, Programmers, and Assignments:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy import Column, ForeignKey
from sqlalchemy.types import Integer, String, Text
from sqlalchemy.orm import relation
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Assignment(Base):
__tablename__ = 'assignment'
id = Column(Integer, primary_key=True)
description = Column(Text)
programmer_id = Column(Integer, ForeignKey('programmer.id'))
project_id = Column(Integer, ForeignKey('project.id'))
def __init__(self, description=description):
self.description = description
def __repr__(self):
return '<Assignment("%s")>' % self.description
class Programmer(Base):
__tablename__ = 'programmer'
id = Column(Integer, primary_key=True)
name = Column(String(64))
assignments = relation("Assignment", backref='programmer')
def __init__(self, name=name):
self.name = name
def __repr__(self):
return '<Programmer("%s")>' % self.name
class Project(Base):
__tablename__ = 'project'
id = Column(Integer, primary_key=True)
name = Column(String(64))
description = Column(Text)
assignments = relation("Assignment", backref='project')
def __init__(self, name=name, description=description):
self.name = name
self.description = description
def __repr__(self):
return '<Project("%s", "%s...")>' % (self.name, self.description[:10])
engine = create_engine('sqlite://')
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
Projects have many Assignments.
Programmers have many Assignments. (understatement?)
But in my office at least, Programmers also have many Projects - I'd like this relationship to be inferred through the Assignments assigned to the Programmer.
I'd like the Programmer model to have a attribute "projects" which will return a list of Projects associated to the Programmer through the Assignment model.
me = session.query(Programmer).filter_by(name='clay').one()
projects = session.query(Project).\
join(Project.assignments).\
join(Assignment.programmer).\
filter(Programmer.id==me.id).all()
How can I describe this relationship clearly and simply using the sqlalchemy declarative syntax?
Thanks!
There are two ways I see:
Define a relation Programmer.projects with secondary='assignment'.
I define Assignment.project as relation and Programmer.projects as association_proxy('assignments', 'project') (probably you'd also like to define a creator). See Simplifying Association Object Relationships chapter for more information.
Related
I have the following simplified database access layer and two tables:
class DataAccessLayer():
def __init__(self):
conn_string = "mysql+mysqlconnector://root:root#localhost/"
self.engine = create_engine(conn_string)
Base.metadata.create_all(self.engine)
Session = sessionmaker()
Session.configure(bind=self.engine)
self.session = Session()
class MatchesATP(Base):
__tablename__ = "matches_atp"
__table_args__ = {"schema": "belgarath", "extend_existing": True}
ID_M = Column(Integer, primary_key=True)
ID_T_M = Column(Integer, ForeignKey("oncourt.tours_atp.ID_T"))
class TournamentsATP(Base):
__tablename__ = "tours_atp"
__table_args__ = {"schema": "oncourt", "extend_existing": True}
ID_T = Column(Integer, primary_key=True)
NAME_T = Column(String(255))
I want to be able to switch the schema names for the two tables to test databases as follows:
belgarath to belgarath_test
oncourt to oncourt_test
I've tried adding:
self.session.connection(execution_options={"schema_translate_map": {"belgarath": belgarath, "oncourt": oncourt}})
To the bottom of DataAccessLayer and then initialising the class with two variables as follows:
def __init__(self, belgarath, oncourt):
However, when I build the following query:
dal = DataAccessLayer("belgarath_test", "oncourt_test")
query = dal.session.query(MatchesATP)
print(query)
I get the following SQL:
SELECT belgarath.matches_atp.`ID_M` AS `belgarath_matches_atp_ID_M`, belgarath.matches_atp.`ID_T_M` AS `belgarath_matches_atp_ID_T_M`
FROM belgarath.matches_atp
This is still referencing the belgarath table.
I also can't figure out a way of changing the schema of the foreign key of oncourt.tours_atp.ID_T at the same time as the tables.
Are there individual solutions or a combined solution to my issues?
You might wanna decorate your subclassed Base declarative model with the #declared_attr decorator.
Try this--
In a base class for your models, say __init__.py...
from sqlalchemy.ext.declarative import declarative_base, declared_attr
SCHEMA_MAIN = 'belgarath' # figure out how you want to retrieve this
SCHEMA_TEST = 'belgarath_test'
class _Base(object):
#declared_attr
def __table_args__(cls):
return {'schema': SCHEMA_MAIN}
...
Base = declarative_base(cls=_Base)
Base.metadata.schema = SCHEMA_MAIN
Now that you have a Base that subclasses _Base with the main schema already defined, all your other models will subclass Base and do the following:
from . import Base, declared_attr, SCHEMA_TEST
class TestModel(Base):
#declared_attr
def __table_args__(cls):
return {'schema': SCHEMA_TEST}
Changing a schema for a foreign key could look like this:
class TournamentsATP(Base):
__tablename__ = "tours_atp"
__table_args__ = {"schema": "oncourt", "extend_existing": True}
ID_T = Column(Integer, primary_key=True)
NAME_T = Column(String(255))
match_id = Column('match_id', Integer, ForeignKey(f'{__table_args__.get("schema")}.matches_atp.id'))
Where match_id is a foreign key to matches_atp.id by using the __table_args[schema] element defined at the class level via #declared_attr.
It only took me 18 months to figure this out. Turns out I needed to add the schema_translate_map to an engine and then create the session with this engine:
from sqlalchemy import create_engine
engine = create_engine(conn_str, echo=False)
schema_engine = engine.execution_options(schema_translate_map={<old_schema_name>: <new_schema_name>})
NewSession = sessionmaker(bind=schema_engine)
session = NewSession()
All ready to roll...
Assuming your goal is to:
have dev/test/prod schemas on a single mysql host
allow your ORM classes to be flexible enough to be used in three different environments without modification
Then John has you most of the way to one type of solution. You could use #declared_attr to dynamically generate __table_args__ as he has suggested.
You could also consider using something like flask-sqlalchemy that comes with a built-in solution for this:
import os
DB_ENV = os.getenv(DB_ENV)
SQLALCHEMY_BINDS = {
'belgarath': 'mysql+mysqlconnector://root:root#localhost/belgarath{}'.format(DB_ENV),
'oncourt': 'mysql+mysqlconnector://root:root#localhost/oncourt{}'.format(DB_ENV)
}
class MatchesATP(Base):
__bind_key__ = "belgarath"
ID_M = Column(Integer, primary_key=True)
ID_T_M = Column(Integer, ForeignKey("oncourt.tours_atp.ID_T"))
class TournamentsATP(Base):
__bind_key__ = "oncourt"
ID_T = Column(Integer, primary_key=True)
NAME_T = Column(String(255))
Basically this method allows you to create a link to a schema (a bind key), and that schema is defined at run-time via the connection string. More information at the flask-sqlalchemy link.
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).
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.
I use SQLalchemy as my ORM and am trying to port my test fixtures to factory_boy. My schema includes two objects in a one-to-many relation. I.e. instances of one model have list like structures with instances of the other. Example:
class Person(...):
id = Column(Integer, primary_key=True)
name = Column(Text)
[...]
class Address(...):
id = Column(Integer, primary_key=True)
city = Column(Text)
[...]
person_id = Column(Integer, ForeignKey('person.id'))
person = relationship("Person", backref="addresses")
Now I am trying to create a factory which creates persons with a couple of addresses. Factory_boy has the SubFactory. But I only see how you can use that in a one-to-one relationship. I know I can create the addresses with a separate factory and then attach them, but I would like to do something like person =PersonFactory.create(num_addresses=4)`.
Does anyone know if this is currently possible in factory_boy?
I use factory_boy 2.4.1.
I am using this pattern in my project. Assuming you already have AddressFactory.
https://factoryboy.readthedocs.io/en/latest/reference.html?highlight=post_generation#factory.post_generation
class PersonFactory(factory.alchemy.SQLAlchemyFactory):
class Meta:
model = Person
#factory.post_generation
def addresses(obj, create, extracted, **kwargs):
if not create:
return
if extracted:
assert isinstance(extracted, int)
AddressFactory.create_batch(size=extracted, person_id=self.id, **kwargs)
Usage
PersonFactory(addresses=4)
This will create Person with 4 Addresses
Also this can accept kwargs
PersonFactory(addresses=2, addresses__city='London')
This will create Person with 2 Addresses which have city field set to 'London'
Here is blog post which may help https://simpleit.rocks/python/django/setting-up-a-factory-for-one-to-many-relationships-in-factoryboy/
#Kristen pointed to the right direction, but AdderssFactory didn't related to Person.
In Django we can use post_generation decorator like this.
class PersonFactory(BaseFactory):
#factory.post_generation
def addresses(self, create, extracted, **kwargs):
self.addresses_set.add(AddressFactory(person=self))
I had this exact question and was disappointed in the lack of good answers here. Turns out it is possible! Leaving this here for those who have the same question.
First, your model needs to define the relationship on the opposite model from the ForeignKey, so it should look like:
class Person(...):
id = Column(Integer, primary_key=True)
name = Column(Text)
addresses = relationship("Person", backref="person")
[...]
class Address(...):
id = Column(Integer, primary_key=True)
city = Column(Text)
[...]
person_id = Column(Integer, ForeignKey('person.id'))
Then, on your PersonFactory, you can add a post_generation hook like this:
class PersonFactory(BaseFactory):
[...attributes...]
#factory.post_generation
def addresses(self, create, extracted, **kwargs):
return AddressFactory.create_batch(4)
and replace the '4' with whatever number you want. Obviously, you need to define the AddressFactory as well.
Currently, there is no way to implement a "many-to-one RelatedFactory" such that it is "baked into your factory"...
That said, this behavior can be implemented with a bit of hackery when instantiating your PersonFactory.
The following recipe will get you what you are looking for:
from sqlalchemy import create_engine, Integer, Text, ForeignKey, Column
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, scoped_session, sessionmaker
import factory
from factory.alchemy import SQLAlchemyModelFactory as sqla_factory
import random
engine = create_engine("sqlite:////tmp/factory_boy.sql")
session = scoped_session(sessionmaker(bind=engine))
Base = declarative_base()
class Person(Base):
id = Column(Integer, primary_key=True)
name = Column(Text)
addresses = relationship("Address", backref="person")
class Address(Base):
id = Column(Integer, primary_key=True)
street = Column(Text)
street_number = Column(Integer)
person_id = Column(Integer, ForeignKey('person.id'))
class AddressFactory(sqla_factory):
class Meta:
model = Address
sqlalchemy_session = session
street_number = random.randint(0, 10000)
street = "Commonwealth Ave"
class PersonFactory(sqla_factory):
class Meta:
model = Person
sqlalchemy_session = session
name = "John Doe"
Base.metadata.create_all(engine)
for i in range(100):
person = PersonFactory(addresses=AddressFactory.create_batch(3))
You could use the solution described here: http://factoryboy.readthedocs.org/en/latest/recipes.html#reverse-dependencies-reverse-foreignkey
Basically, just declare a few RelatedFactory on your PersonFactory:
class PersonFactory(factory.alchemy.SQLAlchemyFactory):
class Meta:
model = Person
address_1 = factory.RelatedFactory(AddressFactory, 'person')
address_2 = factory.RelatedFactory(AddressFactory, 'person')
I wish to create a mapped attribute of an object which is populated from another table.
Using the SQLAlchemy documentation example, I wish to make a user_name field exist on the Address class such that it can be both easily queried and easily accessed (without a second round trip to the database)
For example, I wish to be able to query and filter by user_name Address.query.filter(Address.user_name == 'wcdolphin').first()
And also access the user_name attribute of all Address objects, without performance penalty, and have it properly persist writes as would be expected of an attribute in the __tablename__
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50))
addresses = relation("Address", backref="user")
class Address(Base):
__tablename__ = 'addresses'
id = Column(Integer, primary_key=True)
email = Column(String(50))
user_name = Column(Integer, ForeignKey('users.name'))#This line is wrong
How do I do this?
I found the documentation relatively difficult to understand, as it did not seem to conform to most examples, especially the Flask-SQLAlchemy examples.
You can do this with a join on the query object, no need to specify this attribute directly. So your model would look like:
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import sessionmaker, relation
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
engine = create_engine('sqlite:///')
Session = sessionmaker(bind=engine)
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50))
addresses = relation("Address", backref="user")
class Address(Base):
__tablename__ = 'addresses'
id = Column(Integer, primary_key=True)
email = Column(String(50))
user_id = Column(Integer, ForeignKey("users.id"))
Base.metadata.create_all(engine)
A query after addresses with filtering the username looks like:
>>> session = Session()
>>> session.add(Address(user=User(name='test')))
>>> session.query(Address).join(User).filter(User.name == 'test').first()
<__main__.Address object at 0x02DB3730>
Edit: As you can directly access the user from an address object, there is no need for directly referencing an attribute to the Address class:
>>> a = session.query(Address).join(User).filter(User.name == 'test').first()
>>> a.user.name
'test'
If you truly want Address to have a SQL enabled version of "User.name" without the need to join explicitly, you need to use a correlated subquery. This will work in all cases but tends to be inefficient on the database side (particularly with MySQL), so there is possibly a performance penalty on the SQL side versus using a regular JOIN. Running some EXPLAIN tests may help to analyze how much of an effect there may be.
Another example of a correlated column_property() is at http://docs.sqlalchemy.org/en/latest/orm/mapped_sql_expr.html#using-column-property.
For the "set" event, a correlated subquery represents a read-only attribute, but an event can be used to intercept changes and apply them to the parent User row. Two approaches to this are presented below, one using regular identity map mechanics, which will incur a load of the User row if not already present, the other which emits a direct UPDATE to the row:
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
Base= declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50))
addresses = relation("Address", backref="user")
class Address(Base):
__tablename__ = 'addresses'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('users.id'))
email = Column(String(50))
Address.user_name = column_property(select([User.name]).where(User.id==Address.id))
from sqlalchemy import event
#event.listens_for(Address.user_name, "set")
def _set_address_user_name(target, value, oldvalue, initiator):
# use ORM identity map + flush
target.user.name = value
# use direct UPDATE
#object_session(target).query(User).with_parent(target).update({'name':value})
e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)
s = Session(e)
s.add_all([
User(name='u1', addresses=[Address(email='e1'), Address(email='e2')])
])
s.commit()
a1 = s.query(Address).filter(Address.user_name=="u1").first()
assert a1.user_name == "u1"
a1.user_name = 'u2'
s.commit()
a1 = s.query(Address).filter(Address.user_name=="u2").first()
assert a1.user_name == "u2"