Postgres / SQLAlchemy: no matching unique constraint - python

I am working on a system which requires a composite foreign key in SQLalchemy with Postgres, and have come up with the following example TestCase as a proof of concept. The idea is that every Thing must reference a unique combination of ThingFeatureType, through two columns tt_id (thing type id) and feature (a string). ThingType and Feature also have their own tables.
When I run the code below using pytest, I get the following error complaining that there is no UniqueConstraint on (thing_feature_type.tt_id, feature). However, there definitely is!
Any help on this much appreciated!
Error:
self = <sqlalchemy.dialects.postgresql.psycopg2.PGDialect_psycopg2 object at 0x7f4f61ee4320>, cursor = <cursor object at 0x7f4f61ee1808; closed: -1>
statement = '\nCREATE TABLE thing (\n\tt_id SERIAL NOT NULL, \n\ttt_id INTEGER, \n\tfeature VARCHAR(64), \n\tname VARCHAR(128) NOT...RY KEY (t_id), \n\tFOREIGN KEY(tt_id, feature) REFERENCES thing_feature_type (tt_id, feature) ON DELETE CASCADE\n)\n\n'
parameters = {}, context = <sqlalchemy.dialects.postgresql.psycopg2.PGExecutionContext_psycopg2 object at 0x7f4f5f4a91d0>
def do_execute(self, cursor, statement, parameters, context=None):
> cursor.execute(statement, parameters)
E sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) there is no unique constraint matching given keys for referenced table "thing_feature_type"
E [SQL: '\nCREATE TABLE thing (\n\tt_id SERIAL NOT NULL, \n\ttt_id INTEGER, \n\tfeature VARCHAR(64), \n\tname VARCHAR(128) NOT NULL, \n\tPRIMARY KEY (t_id), \n\tFOREIGN KEY(tt_id, feature) REFERENCES thing_feature_type (tt_id, feature) ON DELETE CASCADE\n)\n\n'] (Background on this error at: http://sqlalche.me/e/f405)
venv/lib/python3.5/site-packages/SQLAlchemy-1.2.7-py3.5-linux-x86_64.egg/sqlalchemy/engine/default.py:507: ProgrammingError
Code:
from unittest import TestCase
from sqlalchemy import (case,
Column,
Float,
ForeignKey,
Integer,
String,
Table,
Text, )
from sqlalchemy.orm import relationship
from sqlalchemy.schema import ForeignKeyConstraint, UniqueConstraint
from concept_back_end.run import app
from concept_back_end.database import db
def define_feature(model):
class Feature(model):
feature = Column(String(64), primary_key=True)
#classmethod
def _define_relationships(cls):
cls.feature_types = relationship('FeatureType',
back_populates='the_feature',
cascade='save-update, delete',
lazy='select')
return Feature
def define_thing_type(model):
class ThingType(model):
tt_id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(128), nullable=False)
#classmethod
def _define_relationships(cls):
cls.things = relationship('Thing',
back_populates='thing_type',
cascade='save-update, delete',
lazy='select')
cls.thing_feature_types = relationship(
'ThingFeatureType',
back_populates='thing_type',
cascade='save-update, delete',
lazy='select'
)
return ThingType
def define_thing_feature_type(model):
class ThingFeatureType(model):
__tablename__ = 'thing_feature_type'
ft_id = Column(Integer, primary_key=True, autoincrement=True)
feature = Column(String(64),
ForeignKey('feature.feature'))
tt_id = Column(Integer, ForeignKey('thing_type.tt_id'))
__table_args__ = (
UniqueConstraint('tt_id', 'feature'),
)
#classmethod
def _define_relationships(cls):
cls.the_feature = relationship('Feature',
back_populates='feature_types',
lazy='select')
cls.thing_type = relationship('ThingType',
back_populates='feature_types',
lazy='select')
cls.things = relationship('Thing',
back_populates='feature_type',
lazy='select')
return ThingFeatureType
def define_thing(model):
class Thing(model):
t_id = Column(Integer, primary_key=True, autoincrement=True)
tt_id = Column(Integer)
feature = Column(String(64))
name = Column(String(128), nullable=False)
__table_args__ = (
ForeignKeyConstraint(
('tt_id', 'feature'),
('thing_feature_type.tt_id', 'thing_feature_type.feature'),
ondelete='CASCADE'
),
{},
)
#classmethod
def _define_relationships(cls):
cls.thing_type = relationship('ThingType',
back_populates='things',
lazy='select')
cls.feature_type = relationship('ThingFeatureType',
back_populates='things',
lazy='select')
return Thing
model_factories = [
define_feature,
define_thing_type,
define_thing_feature_type,
define_thing,
]
"""List of factory functions"""
class ForeignKeyExampleTestCase(TestCase):
def setUp(self):
with app.app_context():
models = [m(db.Model) for m in model_factories]
for m in models:
m._define_relationships()
db.create_all()
db.session.commit()
def test_can_connect_to_db(self):
with app.app_context():
db.session.execute('SELECT * FROM thing;')
def tearDown(self):
"""And then tear them down again"""
with app.app_context():
db.session.close()
db.drop_all()

This appeared to be an issue with stale data in the db. The final working code is below.
from unittest import TestCase
from sqlalchemy import (case,
Column,
Float,
ForeignKey,
ForeignKeyConstraint,
Integer,
String,
Table,
Text,
UniqueConstraint, )
from sqlalchemy.orm import relationship
from sqlalchemy.schema import CreateTable
from concept_back_end.run import app
from concept_back_end.database import db
def define_feature(model):
class Feature(model):
feature = Column(String(64), primary_key=True, unique=True)
#classmethod
def _define_relationships(cls):
cls.feature_types = relationship('FeatureType',
back_populates='the_feature',
cascade='save-update, delete',
lazy='select')
return Feature
def define_thing_type(model):
class ThingType(model):
tt_id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(128), nullable=False)
#classmethod
def _define_relationships(cls):
cls.things = relationship('Thing',
back_populates='thing_type',
cascade='save-update, delete',
lazy='select')
cls.thing_feature_types = relationship(
'ThingFeatureType',
back_populates='thing_type',
cascade='save-update, delete',
lazy='select'
)
return ThingType
def define_thing_feature_type(model):
class ThingFeatureType(model):
__tablename__ = 'thing_feature_type'
__table_args__ = (
UniqueConstraint('tt_id', 'feature'),
{},
)
ft_id = Column(Integer, primary_key=True, autoincrement=True)
feature = Column(String(64),
ForeignKey('feature.feature'))
tt_id = Column(Integer, ForeignKey('thing_type.tt_id'))
#classmethod
def _define_relationships(cls):
cls.the_feature = relationship('Feature',
back_populates='feature_types',
lazy='select')
cls.thing_type = relationship('ThingType',
back_populates='feature_types',
lazy='select')
cls.things = relationship('Thing',
back_populates='feature_type',
lazy='select')
return ThingFeatureType
def define_thing(model):
class Thing(model):
t_id = Column(Integer, primary_key=True, autoincrement=True)
tt_id = Column(Integer)
feature = Column(String(64))
name = Column(String(128), nullable=False)
__table_args__ = (
ForeignKeyConstraint(
['tt_id', 'feature'],
['thing_feature_type.tt_id', 'thing_feature_type.feature'],
ondelete='CASCADE'
),
{},
)
#classmethod
def _define_relationships(cls):
cls.thing_type = relationship('ThingType',
back_populates='things',
lazy='select')
cls.feature_type = relationship('ThingFeatureType',
back_populates='things',
lazy='select')
return Thing
model_factories = [
define_feature,
define_thing_type,
define_thing_feature_type,
define_thing,
]
"""List of factory functions"""
class ForeignKeyExampleTestCase(TestCase):
def setUp(self):
with app.app_context():
models = [m(db.Model) for m in model_factories]
for i, m in enumerate(models):
m._define_relationships()
models[i] = m
for m in models:
print(CreateTable(m.__table__))
db.create_all()
db.session.commit()
def test_can_connect_to_db(self):
with app.app_context():
db.session.execute('SELECT * FROM thing;')
def tearDown(self):
"""And then tear them down again"""
with app.app_context():
db.session.close()
db.drop_all()

Related

AttributeError in defining SQLAlchemy M:N relationship

I have defined M:M relationship as:
association_table = Table('content.association', base.metadata,
Column('content_id', UUID(as_uuid=True),
ForeignKey('content.content.content_id')),
Column('message_id', UUID(as_uuid=True), ForeignKey('messages.message.message_id'))
)
class Content(base):
__tablename__ = "content"
__table_args__ = {"schema": "content"}
id = Column("content_id", UUID(as_uuid=True), primary_key=True)
children = relationship("Messages", secondary=association_table, back_populates="parents")
class Messages(base):
__table_args__ = {"schema": "messages"}
__tablename__ = 'message'
id = Column('message_id', UUID(as_uuid=True), primary_key=True)
url = Column(String)
parents = relationship(
Content,
secondary=association_table,
back_populates="children")
When i try to run query:
def get_all(self):
return self.session.query(Content.children).all()
AttributeError: 'ContentRepository' object has no attribute 'children'
class ContentRepository:
def __init__(self, session):
self.session = session
def get_all(self):
return self.session.query(Content).all()
def get_children(self):
return self.session.query(Content.children).all()
get_children gives the error, before that get_all was working fine.

cannot get one to many relationship working in (Flask-) SQLAlchemy

I have several classes:
import uuid
from app import db, create_app
from sqlalchemy.sql import func
from sqlalchemy.dialects.postgresql import UUID, ARRAY, JSONB
class Ticket(db.Model):
__tablename__ = 'tickets'
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
time = db.Column(db.DateTime, server_default=func.now(), index=True)
proposed_names = db.Column(ARRAY(db.String))
measurements = db.relationship('TempMeasurement', back_populates='ticket')
class BaseMeasurement(object):
id = db.Column(db.Integer, primary_key=True)
#declared_attr
def type_id(self):
return db.Column(db.Integer, db.ForeignKey('optical_data_types.id'))
#declared_attr
def type(self):
return db.relationship('OpticalDataType')
#declared_attr
def operator_id(self):
return db.Column(db.Integer, db.ForeignKey('operators.id'))
#declared_attr
def operator(self):
return db.relationship('Operator')
#declared_attr
def item_id(self):
return db.Column(db.String, db.ForeignKey('items.serial'))
#declared_attr
def item(self):
return db.relationship('Item')
time = db.Column(db.DateTime, index=True)
instrument = db.Column(db.String)
instrument_sn = db.Column(db.String)
data = db.Column(JSONB)
class TempMeasurement(db.Model, BaseMeasurement):
__tablename__ = 'ticket_data'
id = db.Column(db.Integer, primary_key=True)
ticket_id = db.Column(UUID(as_uuid=True), db.ForeignKey('tickets.id'), index=True)
ticket = db.relationship('Ticket', back_populates='measurements')
original_paths = db.Column(ARRAY(db.String))
What I want/expect is that I can create a Ticket with several child TempMeasurements and commit this to the database. Something like:
app = create_app()
with app.app_context():
ticket = Ticket()
ticket.measurements = [TempMeasurement(...)]
db.session.add(ticket) # <-- error on this line
db.session.commit()
However, I get an obscure error deep in SQLAlchemy:
AttributeError: 'str' object has no attribute '_sa_instance_state'
with a full trace here.
I thought that it might be because the UUID ticket_id column has as_uuid, so I made it simply UUID (implicitly a str), but this did not solve my issue.
The error is too deep in SQLAlchemy for me to understand -- can anyone help?

SQLAlchemy: How to use order_by based on a calculated value?

So I have the following database model:
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
nickname = db.Column(db.String(64), nullable=False, unique=True)
...
tasks = db.relationship('Task', backref='author', lazy='dynamic')
def number_of_tries(self):
return Task.query.filter_by(user_id=self.id).count()
def correct_answers(self):
return Task.query.filter_by(user_id=self.id).filter_by(correct=True).count()
def percentage(self):
try:
return '{:.2%}'.format(self.correct_answers()/self.number_of_tries())
except ZeroDivisionError:
return None
def __repr__(self):
return '<User %r>' % (self.nickname)
class Task(db.Model):
id = db.Column(db.Integer, primary_key=True)
task_id = db.Column(db.Integer)
correct = db.Column(db.Boolean)
timestamp = db.Column(db.DateTime)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
def __repr__(self):
return '<%r %s>' % (self.task_id, self.correct)
Two tables, the first one storing User data, the second one in relation with the first, storing data about questions the User answers.
I would like to sort the users based on their success ratio, so I would do something like this:
User.query.order_by(User.percentage).all()
but it won't work. What am I doing wrong? Is it even possible to use order_by based on a method like that?
Even with SqlAlchemy, you have to think in sets of objects and their values. The query you want involves three different sets: Users, their correct answers and their total answers.
Want you want is a query like that (warning, that's just a sample, you could write it much better)
select userid, cor_count/ans_count from users
inner join (select userid, count(*) cor_count from answers where correct=true group by userid) as correct_answers on users.userid=correct_answers.userid
inner join (select userid, count(*) as ans_count from answers group by userid) as total_answers on total_answers.userid=users.userid
where users.userid='xxxx'
order by 2
so, you have to formulate this (somehow) in SqlAlchemy. A guideline would by something to that effect:
ans_q = session.query(Task.user_id, func.count(task.id).label('cnt')).group_by(Task.user_id)
corr_ans_q = session.query(Task.user_id, func.count(task.id).label('cnt')).filter(Task.correct).group_by(Task.user_id)
ans_q = alias(ans_q.selectable)
corr_ans_q = alias(corr_ans_q.selectable)
q = session.query(User).join(ans_q,ans_q.c.user_id==User.id).join(corr_ans_q.c.user_id==User.id).order_by(corr_ans_q.c.cnt/ans_q.c.cnt)
I think that "Hybrid Attributes" will solve you problem.
http://docs.sqlalchemy.org/en/latest/orm/extensions/hybrid.html#module-sqlalchemy.ext.hybrid
UPD:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
nickname = Column(String(64), nullable=False, unique=True)
tasks = relationship('Task', backref='author', lazy='dynamic')
def number_of_tries(self):
return self.tasks.count()
def correct_answers(self):
return self.tasks.filter_by(correct=True).count()
def percentage(self):
try:
return '{:.2%}'\
.format(float(self.correct_answers())/self.number_of_tries())
except ZeroDivisionError:
return None
def __repr__(self):
return '<User %r>' % (self.nickname)
class Task(Base):
__tablename__ = 'task'
id = Column(Integer, primary_key=True)
task_id = Column(Integer)
correct = Column(Boolean)
timestamp = Column(DateTime)
user_id = Column(Integer, ForeignKey('user.id'))
def __repr__(self):
return '<%r %s>' % (self.task_id, self.correct)
from sqlalchemy import create_engine
engine = create_engine('sqlite:///:memory:')
Base.metadata.create_all(bind=engine)
from sqlalchemy.orm import sessionmaker
# create a configured "Session" class
Session = sessionmaker(bind=engine)
# create a Session
session = Session()
from datetime import datetime
task1 = Task(task_id=1, correct=True, timestamp=datetime.now())
task2 = Task(task_id=2, correct=False, timestamp=datetime.now())
task3 = Task(task_id=3, correct=False, timestamp=datetime.now())
task4 = Task(task_id=4, correct=False, timestamp=datetime.now())
task5 = Task(task_id=5, correct=True, timestamp=datetime.now())
task6 = Task(task_id=6, correct=True, timestamp=datetime.now())
user1 = User(nickname="Lekha", tasks=[task1, task2, task3])
user2 = User(nickname="Vanya", tasks=[task4, task5, task6])
session.add(user1, user2)
session.commit()
print(user1.number_of_tries())
print(user1.correct_answers())
print(user1.percentage())

sqlalchemy setting a relationship

I am trying to create a relationship between the two tables.
When instantiating a model I get the following error:
from core.models import Activation
a = Activation()
ArgumentError: Class object expected, got 'Table(u'activations', MetaData(bind=Engine(postgresql+psycopg2://localhost:5432/mydb)), Column('id', Integer(), table=<activations>, primary_key=True, nullable=False), Column('valid_until', DateTime(), table=<activations>), Column('code', Unicode(length=30), table=<activations>, nullable=False), Column('created_by', Unicode(length=16), table=<activations>, nullable=False), schema=None)'.
core/models.py
class ActivationMixin(Base):
#declared_attr
def code(self):
return Column(Unicode(30), nullable=False, unique=True)
#declared_attr
def valid_until(self):
return Column(DateTime, nullable=True)
#declared_attr
def created_by(self):
return Column(Unicode(16), nullable=False)
#classmethod
def get_by_code(cls, request, code):
session = get_session(request)
return session.query(cls).filter(cls.code == code).first()
def __init__(self, created_by='web', valid_until=None):
"""Create a new activation. *valid_until* is a datetime.
It defaults to 3 days from current day.
"""
self.code = generate_random_string(24)
self.created_by = created_by
if valid_until:
self.valid_until = valid_until
else:
self.valid_until = datetime.utcnow() + timedelta(days=3)
class Activation(ActivationMixin):
pass
user/models.py
class User(Base):
email = Column(Unicode(256), nullable=False, unique=True)
status = Column(Boolean, default=False)
salt = Column(Unicode(32), nullable=False)
_password = Column('password', Unicode(256), nullable=False)
logins = Column(Integer, default=0)
last_login = Column(
TIMESTAMP(timezone=False),
default=func.now(),
server_default=func.now(),
nullable=False
)
account_type = Column(AccountType.db_type())
#declared_attr
def activation_id(self):
return Column(
Integer,
ForeignKey('%s.id' % (Activation.__tablename__))
)
#property
def is_activated(self):
if self.activation_id is None:
return True
return False
#declared_attr
def activation(self):
return relationship(
Activation.__tablename__,
backref=self.__tablename__
)
The error is occures with the following declaration:
#declared_attr
def activation(self):
return relationship(
Activation.__tablename__,
backref=self.__tablename__
)
There seems to be some code that you haven't posted since running just this code would give you other errors as well (e.g., related to the __tablename__ attributes not being properly set). However, the actual error you are getting is probably related to this code...
#declared_attr
def activation(self):
return relationship(
Activation.__tablename__,
backref=self.__tablename__
)
According to the documentation for relationship function, the first argument should be...
a mapped class, or actual Mapper instance, representing the target of
the relationship.
However, you are using the __tablename__ attribute, which should be just by the name of the table (a string).
So, try changing this to...
#declared_attr
def activation(self):
return relationship(
Activation,
backref=self.__tablename__
)

Outer Join with sqlalchemy (ORM)

I am trying to do a relationship() with an OUTER JOIN so that it joins the second table if there is something to join it with. I am currently stuck on how to do this though, I cannot seem to figure out the right combination of options(), relationship() and outerjoin().
I have the following tables and I am trying to join AppLike to Application if a row exists with the Application ID AND the artistID (which is provided by the function)
Happy to provide any additional information, I already have one of my joins working as you can see below, but there will always be a row to match for that one.
from sqlalchemy import Column
from . import Base
from . import DBSession
from sqlalchemy.dialects.mysql import (
INTEGER,
VARCHAR,
TEXT,
TINYINT,
)
from sqlalchemy.sql import and_
from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship, joinedload
import time
# 0 = new
# 1 = Denied
# 2 = Accepted
def getNewApplications(artistID):
query = DBSession.query(Application).\
options(joinedload('pieces')).\
options(joinedload('vote')).\
filter(AppLike.artist_id==artistID).\
filter(Application.approved==0)
#join(AppPiece, Application.app_id==AppPiece.app_id).\
#outerjoin(AppLike, and_(Application.app_id==AppLike.app_id,
# AppLike.artist_id==artistID)).\
import pdb; pdb.set_trace()
return query.all()
class Application(Base):
""" The SQLAlchemy declarative model class for a FileFavorite object. """
__tablename__ = 'applications'
__table_args__ = {
'mysql_engine': 'InnoDB',
'mysql_charset': 'utf8'
}
app_id = Column(INTEGER(11), autoincrement=True, primary_key=True, nullable=False)
name = Column(VARCHAR(64), nullable=False)
nickname = Column(VARCHAR(64), nullable=False)
email = Column(VARCHAR(255), nullable=False)
description = Column(TEXT(), nullable=False)
profile_link = Column(VARCHAR(128), nullable=False)
location = Column(VARCHAR(64), nullable=False)
approved = Column(TINYINT(4), nullable=False)
pieces = relationship("AppPiece", lazy='joined')
vote = relationship("AppLike", lazy='joined')
def __init__(self, name, nickname, email, desc, profileLink,
location, approved):
self.name = name
self.nickname = nickname
self.email = email
self.description = desc
self.profile_link = profileLink
self.location = location
self.approved = approved
class AppPiece(Base):
""" The SQLAlchemy declarative model class for a FileFavorite object. """
__tablename__ = 'app_pieces'
__table_args__ = {
'mysql_engine': 'InnoDB',
'mysql_charset': 'utf8'
}
app_piece_id = Column(INTEGER(11), autoincrement=True, primary_key=True, nullable=False)
app_id = Column(INTEGER(11), ForeignKey('applications.app_id'))
link = Column(VARCHAR(128), nullable=False)
def __init__(self, appID, link):
self.app_id = appID
self.link = link
class AppLike(Base):
""" The SQLAlchemy declarative model class for a FileFavorite object. """
__tablename__ = 'app_likes'
__table_args__ = {
'mysql_engine': 'InnoDB',
'mysql_charset': 'utf8'
}
app_id = Column(INTEGER(11), ForeignKey('applications.app_id'))
artist_id = Column(INTEGER(11), primary_key=True, nullable=False)
vote = Column(TINYINT(4), nullable=False)
def __init__(self, appID, artistID, vote):
self.app_id = appID
self.artist_id = artistID
self.vote = vote
You definitely don't need options(joinedload('pieces')), it is already defined in your models (lazy='joined'). The join condition is the tricky part here and needs to be done using subquery, since we want to filter there as well. So, the final query should look something like this:
# We do the filtering on AppLike in the subquery and later join
# Application to it.
applike_subq = DBSession.query(AppLike).\
filter(AppLike.artist_id == artistID).subquery()
query = DBSession.query(Application).\
outerjoin(applike_subq, Application.vote).\
filter(Application.approved == 0).all()

Categories

Resources