Materialized path relationship in declarative SQLAlchemy - python

I have a hierarchical categories model, where hierarchy is maintained using materialized path (one character per level):
class Category(Base):
__tablename__ = 'categories'
id = Column(SmallInteger, primary_key=True)
path = Column(String, unique=True, nullable=False)
# problematic relationship
all_subcats = relationship('Category', lazy='dynamic', viewonly=True,
primaryjoin=foreign(path).like(remote(path).concat('%')))
When trying to define "all subcategories" relationship I run into a problem:
sqlalchemy.exc.ArgumentError: Can't determine relationship direction for
relationship 'Category.all_subcats' - foreign key columns within the join
condition are present in both the parent and the child's mapped tables.
Ensure that only those columns referring to a parent column are marked as
foreign, either via the foreign() annotation or via the foreign_keys argument.
SQLAlchemy is confused, because I'm joining on the same column. All examples I've managed to find always join on different columns.
Is this sort of relationship possible at all? I want to query through this join, so custom #property is not acceptable.

Use the latest git master or version 0.9.5 or greater of SQLAlchemy. Then:
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Element(Base):
__tablename__ = 'element'
path = Column(String, primary_key=True)
related = relationship('Element',
primaryjoin=
remote(foreign(path)).like(
path.concat('/%')),
viewonly=True,
order_by=path)
e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)
sess = Session(e)
sess.add_all([
Element(path="/foo"),
Element(path="/foo/bar1"),
Element(path="/foo/bar2"),
Element(path="/foo/bar2/bat1"),
Element(path="/foo/bar2/bat2"),
Element(path="/foo/bar3"),
Element(path="/bar"),
Element(path="/bar/bat1")
])
e1 = sess.query(Element).filter_by(path="/foo/bar2").first()
print [e.path for e in e1.related]
note that this model, whether you deal with "descendants" or "anscestors", uses collections. You want to keep remote() and foreign() together so that the ORM considers it as one to many.

Related

What happens if I specify OneToMany relationship only from one side in flask-sqlalchemy?

I have a OneToMany relationship between 2 entities in flask. I also specified the relationship only on one side. I am unsure what the difference is between the following:
class CustomJob(db.Model):
__tablename__ = "custom_job"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
country_from = db.Column(db.Integer, db.ForeignKey('country.id'))
class Country(db.Model):
__tablename__ = "country"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
custom_jobs = db.relationship('CustomJob', backref="country", lazy=False)
Or just specify the foreign key on master entity:
class CustomJob(db.Model):
__tablename__ = "custom_job"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
country_from = db.Column(db.Integer, db.ForeignKey('country.id'))
will is there performance difference between the two ?
The brilliance behind an ORM like SQLAlchemy is that it can detect relationships between models based on foreign key constraints. So once you've declared your foreign key on the custom_job table, the relationship is configured in the database.
Mapping that relationship to your python objects is another useful part of ORM's. Here, you are doing that with db.relationship. By specifying backref, you are essentially telling the ORM to make the relationship available on the other object.
Let me explain more explicitly using the code provided in your Q:
class Country(db.Model):
__tablename__ = 'country'
...
custom_jobs = db.relationship('CustomJob', backref='custom_job', lazy=False)
...
The Country model you've defined will map all associated rows from the custom_job table through the attribute Country.custom_jobs.
This relationship will propagate to the CustomJob model and allow you to access the associated rows from the country table through an attribute created by the backref parameter --> here CustomJob.custom_job.
I assume this is an error and that you intended to use backref="country"
In this case, access associated objects instead with CustomJob.country

Limiting returned records on a selectinload SQLAlchemy relationship

Basing this question off this similar post, but about SORT ORDER
I understand that you can change the lazy to dynamic in the relatonship, and then that will allow you to query against the relationship before loading, but is there a way to LIMIT the return results directly from a selectin or on of the other loading techniques?
Use case is, Im trying to pass the record into Marshmallow and limit the number of nested records returned. dynamic at that point doesnt work, as Marshmallow includes it as an all() and selectin appears to just included it unqueryable at time of load, and again Marshmallow get the entire record set.
from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class Example(Base):
__tablename__ = 'examples'
id = Column(Integer, primary_key=True)
related_items = relationship('RelatedItem', back_populates='example', order_by='RelatedItem.id')
class RelatedItem(Base):
__tablename__ = 'related_items'
id = Column(Integer, primary_key=True)
example_id = Column(Integer, ForeignKey('examples.id'), nullable=False)
example = relationship('Example', back_populates='related_items')

Is it possible to rename the metadata attribute of a SQLAlchemy declarative base?

I'm trying to set up a database with a few specific fields (and I can't move away from the specification). One of the fields would be a column called metadata, but sqlalchemy prevents that:
sqlalchemy.exc.InvalidRequestError: Attribute name 'metadata' is reserved for the MetaData instance when using a declarative base class.
Is there a decent workaround for this? Do I need to monkeypatch the declarative_base function to rename the metadata attribute? I couldn't find an option to rename that attribute in the api docs.
Here's some example code that will fail with the above error:
#!/usr/bin/env python3.7
from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy import Column, Integer
class CustomBase(object):
#declared_attr
def __tablename__(cls):
return cls.__name__.lower()
DBBase = declarative_base(cls=CustomBase)
class Data(DBBase):
id = Column(Integer, primary_key=True)
metadata = Column(Integer)
if __name__ == "__main__":
print(dir(Data()))
You can use like:
class Data(DBBase):
id = Column(Integer, primary_key=True)
# metadata = Column(Integer)
metadata_ = Column("metadata", Integer)
The constructor of Column class has a name parameter. You can find it from https://docs.sqlalchemy.org/en/13/core/metadata.html#sqlalchemy.schema.Column
The name field may be omitted at construction time and applied later
In other words, you could write a name as you want originally.

Python SqlAlchemy relationships

I'm using Python 2.7.5 and SqlAlchemy 0.9.9 (against an Oracle 11 database) and am trying to figure out how I can create a relationship between two tables where the join value is a string in one table and an integer in another. Here's mockup of my two tables:
class BatchInput(db.Model):
__tablename__ = 'batchinput'
batchinput_id = Column(Integer, primary_key=True)
item_id = Column(Integer)
class SubBatch(db.Model):
__tablename__ = 'subbatch'
subbatch_id = Column(Integer, primary_key=True)
subbatch_no = Column(String)
batch_input = relationship('BatchInput',
primaryjoin='SubBatch.subbatch_no == cast(BatchInput.item_id, VARCHAR)')
When I run a query to get subbatches I get this message:
Internal Server Error, Could not locate any relevant foreign key
columns for primary join condition
'subbatch.subbatchno = CAST(forgebatchinput.item_id AS VARCHAR)'
on relationship SubBatch.forge_batch_input. Ensure that
referencing columns are associated with a ForeignKey or
ForeignKeyConstraint, or are annotated in the join condition
with the foreign() annotation
I've tried a couple variations of this adding foreignkeys and remote, etc., but always get this message. I'm not sure what it's trying to tell me as I do have a primaryjoin specified.

NoForeignKey error on relationship even though SQLAlchemy model specifies foreign key

I am having some difficulty setting up a one to one relationship between two models in my flask application. I have two models Employeeand `Photo'. An employee has only one photo associated with it and vice-versa.
This is the code that I have in my models.py file:
class Employee(db.Model):
__tablename__ = 'employees'
id = db.Column(db.Integer, primary_key=True)
photo = db.relationship("Photo", uselist=False, back_populates='employees')
class Photo(db.Model):
__tablename__ = 'photos'
id = db.Column(db.Integer, primary_key=True)
employee_id = db.Column(db.Integer, db.ForeignKey('employees.id'))
employee = db.relationship('Photo', back_populates='photo')
I've followed the instruction on the SQL Alchemy documentation found hereSQL Alchemy simple relationships. The error that I keep encountering is shown below:
sqlalchemy.exc.NoForeignKeysError: Could not determine join condition between parent/child tables on relationship Photo.employee
- there are no foreign keys linking these tables.
Ensure that referencing columns are associated with a ForeignKey or ForeignKeyConstraint, or specify a 'primaryjoin' expression.
I clearly specify the foreign key right here employee_id = db.Column(db.Integer, db.ForeignKey('employees.id')) . I'm not sure what I'm doing wrong. Additionally, I was reading the documentation and it doesn't help that uselist, backref, and back_populates are so similar.
Can someone assist me with this? Help would be greatly appreciated.
One to One relationship stack overflow question
backref automatically adds the reverse relationship to the related model. You can pass a db.backref object to it to specify options to the relationship. back_populates tells SQLAlchemy to populate an existing reverse relationship, rather than creating it. uselist tells SQLAlchemy whether the relationship is a list or not, for cases where it can't determine that automatically.
In your example, you need one relationship, with one backref that is a single item.
You have two typos in your code. First, back_populates='employees' should refer to 'employee', which is what you called the property on the related model. Second, employee = relationship('Photo' is pointing at the wrong model, it should relate to Employee.
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
db = SQLAlchemy(app)
db.engine.echo = True
class Photo(db.Model):
id = db.Column(db.Integer, primary_key=True)
class Employee(db.Model):
id = db.Column(db.Integer, primary_key=True)
photo_id = db.Column(db.ForeignKey(Photo.id))
photo = db.relationship(Photo, backref=db.backref('employee', uselist=False))
db.create_all()
db.session.add(Employee(photo=Photo()))
db.session.commit()
print(Employee.query.get(1).photo)

Categories

Resources