SQLAlchemy models assign relation unpredictably - python

I'm creating a database User model which can belong to several Traits. The below script reproduces a strange behavior that I don't understand -- after committing the new rows, the relationship attaches the Traits to the Users in an unpredictable way.
I want to be able to attach 1+ traits to a user, and I want users to be able to share traits.
But in this example, when a trait is shared between users, the Trait sometimes is attached to user 1, and othertimes user 2. How can I make it so that the users can share traits?
import enum
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Enum, UniqueConstraint, ForeignKey
from sqlalchemy.orm import backref, relationship, validates
Base = declarative_base()
class TraitName(enum.Enum):
happy = 0
mad = 1
full = 2
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(80), unique=True, nullable=False)
traits = relationship("Trait")
def __repr__(self):
return self.username + str(traits)
class Trait(Base):
__tablename__ = 'traits'
id = Column(Integer, primary_key=True)
name = Column(Enum(TraitName), nullable=False)
user_id = Column(Integer, ForeignKey('users.id'))
__table_args__ = (UniqueConstraint('name', 'user_id', name='_user_trait'),)
def __repr__(self):
return str(self.name)
When I run this script several times,
from sqlalchemy import create_engine
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
engine = create_engine('postgresql://postgres#localhost:5420/test_db')
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
session = Session(engine)
user1 = User(username="bob")
user2 = User(username="ann")
happy_trait = Trait(name=TraitName.happy)
full_trait = Trait(name=TraitName.full)
user1.traits.extend([full_trait, happy_trait])
user2.traits.append(happy_trait)
session.add_all([user1, user2])
session.commit()
print([t.traits for t in session.query(User).all()])
print(session.query(Trait.user_id).all())
print(session.query(Trait).filter(Trait.user_id.in_((1,))).all())
session.close()
different results:
either:
[[TraitName.full, TraitName.happy], []] # Users' traits
[(1,), (1,)] # Traits' user_ids
[TraitName.full, TraitName.happy] # Traits with uers_id 1
or
[[TraitName.full], [TraitName.happy]] # Users' traits
[(1,), (2,)] # Traits' user_ids
[TraitName.full] # Traits with uers_id 1
So I would like to understand why the Trait will be assigned unpredictably. And how I can model my User and Traits to avoid this -- maybe this isn't the appropraite pattern for attaching traits to users?
Also, if it's possible (I suppose everything is possible), I'd like to have two fields on User: inactive_traits and active_traits fields. But in anycase I first have to figure out how to get one list of traits working.

You named your classes badly, which caused you to use them wrong.
Your TraitName is actually your Trait, which should be shared and your Trait is actually a UserTrait, which can't be shared between users, but you try to share the instance between them.
I'd do the following instead:
import enum
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import orm, Enum, ForeignKey, create_engine
Base = declarative_base()
class Trait(enum.Enum):
happy = 0
mad = 1
full = 2
class User(Base):
__tablename__ = 'users'
user_id = Column(Integer, primary_key=True)
username = Column(String(80), unique=True, nullable=False)
traits = orm.relationship("UserTrait")
def __repr__(self):
return self.username + str(self.traits)
class UserTrait(Base):
__tablename__ = 'usertraits'
user_id = Column(Integer, ForeignKey('users.user_id'), primary_key=True)
trait = Column(Enum(Trait), primary_key=True)
def __repr__(self):
return str([self.user_id, self.trait])
def __main__():
engine = create_engine('postgresql:///?host=/var/run/postgresql/', echo=False)
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
session = orm.Session(engine)
user1 = User(username="bob")
user2 = User(username="ann")
user1.traits.extend([UserTrait(trait=Trait.full), UserTrait(trait=Trait.happy)])
user2.traits.append(UserTrait(trait=Trait.happy))
session.add_all([user1, user2])
session.commit()
print([t.traits for t in session.query(User).all()])
print(session.query(UserTrait.user_id).all())
print(session.query(UserTrait).filter(UserTrait.user_id.in_((1,))).all())
session.close()
if __name__ == "__main__":
__main__()

Related

Ensure that referencing columns are associated with a ForeignKey or ForeignKeyConstraint, or specify a 'primaryjoin' expression

Please could someone help me with this error? I have actually been really struggling to find solid, simple examples for SQLAlchemy. Whilst there are plenty of Model examples of there is not much examples of how to use these Models.
The Error:
sqlalchemy.exc.NoForeignKeysError:
Could not determine join condition between parent/child tables on relationship Species.sc_genus
- there are no foreign keys linking these tables.
Ensure that referencing columns are associated with a ForeignKey or ForeignKeyConstraint, or specify a 'primaryjoin' expression.
The Code
from sqlalchemy import Integer, Column, String, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relation
Base = declarative_base()
class Genus(Base):
__tablename__ = 'genus'
id = Column(Integer, primary_key=True)
common_name = Column(String)
scientific_name = Column(String)
sc_sub_family = "sc_sub_family"
def __repr__(self):
return "<Genus(common_name='%s')>" % (self.scientific_name)
# Species is a child of Genus
class Species(Base):
__tablename__ = 'species'
id = Column(Integer, primary_key=True)
common_name = Column(String)
scientific_name = Column(String)
sc_genus = relation("Genus", backref="species")
def __repr__(self):
return "<Species(common_name='%s')>" % (self.scientific_name)
def addSpecies(session):
species = Species()
species.common_name = "House Cat"
species.scientific_name = "Felis catus"
genus = Genus()
genus.scientific_name = "Felis"
session.add(genus)
species.sc_genus = genus
session.add(species)
session.commit()
if __name__ == "__main__":
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
## A bunch of stuff to make the connection to the database work.
engine = create_engine('sqlite:///foos.db', echo=True)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
addSpecies(session)
I needed to specify a foreign key for the relationship.
class Genus(Base):
__tablename__ = 'genus'
id = Column(Integer, primary_key=True)
scientific_name = Column(String)
# sc_sub_family = "sc_sub_family"
def __repr__(self):
return "<Genus(common_name='%s')>" % (self.scientific_name)
# Species is a child of Genus
class Species(Base):
__tablename__ = 'species'
id = Column(Integer, primary_key=True)
common_name = Column(String)
scientific_name = Column(String)
sc_genus = relation("Genus", backref="species")
sc_genus_id = Column(Integer, ForeignKey('genus.id'))
def __repr__(self):
return "<Species(common_name='%s')>" % (self.scientific_name)

SQLAlchemy association data via related class constructor

I have this design
# coding: utf-8
from sqlalchemy import Column, VARCHAR, ForeignKey, create_engine
from sqlalchemy.dialects.mysql import INTEGER, TINYINT
from sqlalchemy.orm import relationship, sessionmaker
from sqlalchemy.inspection import inspect
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy
engine = create_engine('mysql://root:toor#localhost/test', echo=True)
Model = declarative_base()
Session = sessionmaker(bind=engine)
session = Session()
class A(Model):
__tablename__ = 'A'
id = Column(INTEGER(unsigned=True), primary_key=True)
name = Column(VARCHAR(32), nullable=False, index=True)
class AB(Model):
__tablename__ = 'AB'
A_id = Column(INTEGER(10, unsigned=True), ForeignKey('A.id'), nullable=False, primary_key=True)
B_id = Column(INTEGER(10, unsigned=True), ForeignKey('B.id'), nullable=False, primary_key=True)
type = Column(TINYINT(3, unsigned=True), nullable=False)
A_rel = relationship(
"A",
foreign_keys=[A_id]
)
B_rel = relationship(
"B",
foreign_keys=[B_id],
)
mapper_AB = inspect(AB)
class B(Model):
__tablename__ = 'B'
id = Column(INTEGER(10, unsigned=True), primary_key=True)
As = relationship("A", secondary=mapper_AB.local_table, uselist=False)
ABs = relationship("AB")
type = association_proxy('ABs', 'type')
Model.metadata.create_all(engine)
a = A(
name="test",
)
session.add(a)
session.commit()
b = B(
As=a,
type=2
)
print(b.type)
session.add(b)
session.commit()
This obviously fails, but I am trying to simplify the design by adding the AB extra information (type) via one (B) of the related classes constructor.
I have gone through tons of SQLAlchemy documentation, and even lost myself inside the source code but to no avail. The 3 table design of the database cannot be modified, as is part of a bigger system and belongs to another project. My goal is to map those table into a more simplified design for further development where this models will be used.
Is this possible using SQLAlchemy mechanisms with declarative style?

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.

Validation in SQLAlchemy

How can I get the required validator in SQLAlchemy? Actually I just wanna be confident the user filled all required field in a form. I use PostgreSQL, but it doesn't make sense, since the tables created from Objects in my models.py file:
from sqlalchemy import (
Column,
Integer,
Text,
DateTime,
)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import (
scoped_session,
sessionmaker,
)
from zope.sqlalchemy import ZopeTransactionExtension
from pyramid.security import (
Allow,
Everyone,
)
Base = declarative_base()
class Article(Base):
""" The SQLAlchemy declarative model class for a Article object. """
__tablename__ = 'article'
id = Column(Integer, primary_key=True)
name = Column(Text, nullable=False, unique=True)
url = Column(Text, nullable=False, unique=True)
title = Column(Text)
preview = Column(Text)
content = Column(Text)
cat_id = Column(Integer, nullable=False)
views = Column(Integer)
popular = Column(Integer)
created = Column(DateTime)
def __unicode__(self):
return unicode(self.name)
So this nullable=False doesn't work, because the records added in any case with empty fields. I can of course set the restrictions at the database level by set name to NOT NULL for example. But there must be something about validation in SQLAlchemy isn't it? I came from yii php framework, there it's not the problem at all.
By empty fields I guess you mean an empty string rather than a NULL. A simple method is to add validation, e.g.:
class Article(Base):
...
name = Column(Text, unique=True)
...
#validates('name')
def validate_name(self, key, value):
assert value != ''
return value
To implement it at a database level you could also use a check constraint, as long as the database supports it:
class Article(Base):
...
name = Column(Text, CheckConstraint('name!=""')
...

How to extend the `getter`-functionality of SQLAlchemy's association_proxy?

Edit: I would like to model a 1 to 0:1 relationship between User and Comment (a User can have zero or one Comment). Instead of accessing the object Comment I would rather directly access the comment itself. Using SQLAlchemys association_proxy works perfect for that scenario except for one thing: accessing User.comment before having a Comment associated. But in this case I would rather expect None instead of AttributeError as result.
Look at the following example:
import sqlalchemy as sa
import sqlalchemy.orm as orm
from sqlalchemy import Column, Integer, Text, ForeignKey, Table
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(Text)
def __init__(self, name):
self.name = name
# proxy the 'comment' attribute from the 'comment_object' relationship
comment = association_proxy('comment_object', 'comment')
class Comment(Base):
__tablename__ = 'comments'
id = Column(Integer, primary_key=True)
comment = Column('comment', Text, nullable=False, default="")
user_id = Column(ForeignKey('users.id'), nullable=False, unique=True)
# user_id has to be unique to ensure that a User can not have more than one comments
def __init__(self, comment):
self.comment = comment
user_object = orm.relationship(
"User",
uselist=False, # added after edditing the question
backref=orm.backref('comment_object', uselist=False)
)
if __name__ == "__main__":
engine = sa.create_engine('sqlite:///:memory:', echo=True)
Session = orm.sessionmaker(bind=engine)
Base.metadata.create_all(engine)
session = Session()
Now, the following code throws an AttributeError:
u = User(name="Max Mueller")
print u.comment
What would be the best way to catch that exception and provide a default value instead (like an empty string)?
You don't really need association_proxy for this. You could really get by just fine with a regular property. The AttributeError is (probably) caused because the comment_object is itself None, since there is no dependent row, and None has no comment attribute.
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(Text)
def __init__(self, name):
self.name = name
# proxy the 'comment' attribute from the 'comment_object' relationship
#property
def comment(self):
if self.comment_object is None:
return ""
else:
return self.comment_object.comment
#comment.setter
def comment(self, value):
if self.comment_object is None:
self.comment_object = Comment()
self.comment_object.comment = value
Try this
import sqlalchemy as sa
import sqlalchemy.orm as orm
from sqlalchemy import Column, Integer, Text, ForeignKey, Table
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(Text)
def __init__(self, name):
self.name = name
# proxy the 'comment' attribute from the 'comment_object' relationship
comment = association_proxy('comment_object', 'comment')
class Comment(Base):
__tablename__ = 'comments'
id = Column(Integer, primary_key=True)
comment = Column('comment', Text, nullable=False, default="")
user_id = Column(ForeignKey('users.id'), nullable=False)
def __init__(self, comment):
self.comment = comment
user_object = orm.relationship(
"User",
backref=orm.backref('comment_object'),
uselist=False
)
if __name__ == "__main__":
engine = sa.create_engine('sqlite:///:memory:', echo=True)
Session = orm.sessionmaker(bind=engine)
Base.metadata.create_all(engine)
session = Session()
u = User(name="Max Mueller")
# comment = Comment("")
# comment.user_object = u
# session.add(u)
# session.commit()
print "SS :", u
print u.comment
You gave uselist in backref which must be in relationship.
I do not see any answer that would solve the issue and also would work with "sort_by" for example.
Maybe it is just better to use 'column_property", see Order by association proxy: invalid sql.

Categories

Resources