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

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.

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 models assign relation unpredictably

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__()

How to store data via relationship in constructor of SQLAlchemy model?

How to add objects in the constructor with relationship? The id is not yet ready when constructor is evaluated. In simpler cases it is possible to just provide a list, calculated beforehand. In the example below I tried to say there is a complex_cls_method, in a way it is more like black box.
from sqlalchemy import create_engine, MetaData, Column, Integer, String, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm import sessionmaker
DB_URL = "mysql://user:password#localhost/exampledb?charset=utf8"
engine = create_engine(DB_URL, encoding='utf-8', convert_unicode=True, pool_recycle=3600, pool_size=10)
session = sessionmaker(autocommit=False, autoflush=False, bind=engine)()
Model = declarative_base()
class User(Model):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
simple = Column(String(255))
main_address = Column(String(255))
addresses = relationship("Address",
cascade="all, delete-orphan")
def __init__(self, addresses, simple):
self.simple = simple
self.main_address = addresses[0]
return # because the following does not work
self.addresses = Address.complex_cls_method(
user_id_=self.id, # <-- this does not work of course
key_="address",
value_=addresses
)
class Address(Model):
__tablename__ = 'address'
id = Column(Integer, primary_key=True)
keyword = Column(String(255))
value = Column(String(255))
user_id = Column(Integer, ForeignKey('user.id'), nullable=False)
parent_id = Column(Integer, ForeignKey('address.id'), nullable=True)
#classmethod
def complex_cls_method(cls, user_id_, key_, value_):
main = Address(keyword=key_, value="", user_id=user_id_, parent_id=None)
session.add_all([main])
session.flush()
addrs = [Address(keyword=key_, value=item, user_id=user_id_, parent_id=main.id) for item in value_]
session.add_all(addrs)
return [main] + addrs
if __name__ == "__main__":
# Model.metadata.create_all(engine)
user = User([u"address1", u"address2"], "simple")
session.add(user)
session.flush()
# as it can't be done in constructor, these additional statements needed
user.addresses = Address.complex_cls_method(
user_id_=user.id,
key_="address",
value_=[u"address1", u"address2"]
)
session.commit()
The question is, is there syntactically elegant (and technically sound) way to do this with User's constructor, or is it safer to just call a separate method of User class after session.flush to add desired objects to relationships (as in the example code)?
Giving up on constructor altogether is still possible, but less desirable option as resulting signature change would require significant refactorings.
Instead of manually flushing and setting ids etc. you could let SQLAlchemy handle persisting your object graph. You'll just need one more adjacency list relationship in Address and you're all set:
class User(Model):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
simple = Column(String(255))
main_address = Column(String(255))
addresses = relationship("Address",
cascade="all, delete-orphan")
def __init__(self, addresses, simple):
self.simple = simple
self.main_address = addresses[0]
self.addresses = Address.complex_cls_method(
key="address",
values=addresses
)
class Address(Model):
__tablename__ = 'address'
id = Column(Integer, primary_key=True)
keyword = Column(String(255))
value = Column(String(255))
user_id = Column(Integer, ForeignKey('user.id'), nullable=False)
parent_id = Column(Integer, ForeignKey('address.id'), nullable=True)
# For handling parent/child relationships in factory method
parent = relationship("Address", remote_side=[id])
#classmethod
def complex_cls_method(cls, key, values):
main = cls(keyword=key, value="")
addrs = [cls(keyword=key, value=item, parent=main) for item in values]
return [main] + addrs
if __name__ == "__main__":
user = User([u"address1", u"address2"], "simple")
session.add(user)
session.commit()
print(user.addresses)
Note the absence of manual flushes etc. SQLAlchemy automatically figures out the required order of insertions based on the object relationships, so that dependencies between rows can be honoured. This is a part of the Unit of Work pattern.

Correct pattern for inserting row in SQLAlchemy

I have an insert action that is kinda complex, it boils to:
input: list of tags, book name
INSERT INTO books (book_name) VALUES (book name)
for each tag:
if tag does not exist:
INSERT INTO tags (tag_name) VALUES (tag name)
INSERT INTO books_tags (book_id, tag_id) VALUES (book id, tag id)
Can I do it somehow easier than just writing those in SQLALchemy syntax?
Short Answer: Use custom creator in the definition of association_proxy:
def _tag_find_or_create(tag_name):
tag = Tag.query.filter_by(tag_name=tag_name).first()
return tag or Tag(tag_name=tag_name)
class Book(Base):
__tablename__ = 'book'
id = Column(Integer, primary_key=True)
book_name = Column(String)
# relationship
_tags = relationship('Tag', secondary='book_tag')
tags = association_proxy('_tags', 'tag_name', creator=_tag_find_or_create)
Long Answer: The code below is a self-contained working example. Few words on the implementation:
We use Association Proxy in order to simplify many-to-many relationship. Give this whole page a good read.
In particular, we redefine the default creator function, which will first query the database/session to check for the Tag, and create one if not found.
Code:
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy import UniqueConstraint, ForeignKey
from sqlalchemy.orm import relationship, scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy
# Configure test data SA
engine = create_engine('sqlite:///:memory:', echo=True)
session = scoped_session(sessionmaker(bind=engine))
Base = declarative_base(engine)
Base.query = session.query_property()
def _tag_find_or_create(tag_name):
tag = Tag.query.filter_by(tag_name=tag_name).first()
return tag or Tag(tag_name=tag_name)
class Book(Base):
__tablename__ = 'book'
id = Column(Integer, primary_key=True)
book_name = Column(String)
# relationship
_tags = relationship('Tag', secondary='book_tag')
tags = association_proxy('_tags', 'tag_name', creator=_tag_find_or_create)
class BookTag(Base):
__tablename__ = 'book_tag'
__tableargs__ = (UniqueConstraint('book_id', 'tag_id', name='book_tag_uc'),)
id = Column(Integer, primary_key=True)
book_id = Column(Integer, ForeignKey('book.id'))
tag_id = Column(Integer, ForeignKey('tag.id'))
class Tag(Base):
__tablename__ = 'tag'
id = Column(Integer, primary_key=True)
tag_name = Column(String, unique=True)
# CREATE SCHEMA
Base.metadata.create_all()
def _insert_test_data():
book = Book(book_name="book-1")
book.tags.append("fiction")
book.tags.append("history")
session.add(book)
session.commit()
assert 1 == len(Book.query.all())
assert 2 == len(Tag.query.all())
book2 = Book(book_name="book-2")
book2.tags.append("history")
book2.tags.append("crime")
session.add(book2)
session.commit()
assert 2 == len(Book.query.all())
assert 3 == len(Tag.query.all())
def _add_new_book(book_name, tags):
book = Book.query.filter(Book.book_name == book_name).first()
assert not(book), "Book with name '{}' already found [{}]".format(
book_name, book)
book = Book(book_name=book_name)
for tag in tags:
book.tags.append(tag)
session.add(book)
session.commit()
if __name__ == '__main__':
_insert_test_data()
_add_new_book('SuperMan', ['fiction', 'romance'])

SqlAlchemy relationship to specific columns

Say I have a SqlAlchemy model something like this:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, String, Integer, ForeignKey
from sqlalchemy.orm import sessionmaker, relationship
Base = declarative_base()
Session = sessionmaker()
class EmployeeType(Base):
__tablename__ = 'employee_type'
id = Column(Integer(), primary_key=True)
name = Column(String(20))
class Employee(Base):
__tablename__ = 'employee'
id = Column(Integer(), primary_key=True)
type_id = Column(Integer(), ForeignKey(EmployeeType.id))
type = relationship(EmployeeType, uselist=False)
session = Session()
session.add(EmployeeType(name='drone'))
session.add(EmployeeType(name='PHB'))
I'd like to have some kind of "relationship" from Employee directly to EmployeeType.name as a convenience, so I can skip the step of looking up an id or EmployeeType object if I have a type name:
emp = Employee()
emp.type_name = "drone"
session.add(emp)
session.commit()
assert (emp.type.id == 1)
Is such a thing possible?
EDIT: I found that association_proxy can get me partway there:
class Employee(Base):
...
type_name = association_proxy("type", "name")
the only problem being that if I assign to it:
emp = session.query(Employee).filter_by(EmployeeType.name=='PHB').first()
emp.type_name = 'drone'
it modifies the employee_type.name column, not the employee.type_id column.
I agree with Jonathan's general approach, but I feel like adding an employee object to the session and setting the employee type should be independent operations. Here's an implementation that has type_name as a property and requires adding to the session before setting it:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, String, Integer, ForeignKey
from sqlalchemy.orm import sessionmaker, relationship
Base = declarative_base()
Session = sessionmaker()
class EmployeeType(Base):
__tablename__ = 'employee_type'
id = Column(Integer(), primary_key=True)
name = Column(String(20))
class Employee(Base):
__tablename__ = 'employee'
id = Column(Integer(), primary_key=True)
type_id = Column(Integer(), ForeignKey(EmployeeType.id))
type = relationship(EmployeeType)
#property
def type_name(self):
if self.type is not None:
return self.type.name
return None
#type_name.setter
def type_name(self, value):
if value is None:
self.type = None
else:
session = Session.object_session(self)
if session is None:
raise Exception("Can't set Employee type by name until added to session")
self.type = session.query(EmployeeType).filter_by(name=value).one()
I would do this by creating a method that does this for me.
class EmployeeType(Base):
__tablename__ = 'employee_type'
id = Column(Integer(), primary_key=True)
name = Column(String(20))
class Employee(Base):
__tablename__ = 'employee'
id = Column(Integer(), primary_key=True)
type_id = Column(Integer(), ForeignKey(EmployeeType.id))
type = relationship(EmployeeType, uselist=False)
def __init__(self, type):
self.type = type
def add(self, type_name=None):
if type_name is not None:
emp_type = DBSession.query(EmployeeType).filter(EmployeeType.name == type_name).first()
if emp_type:
type = emp_type
else:
type = EmployeeType(name=type_name)
else:
type = None
DBSession.add(Employee(type=type))
Then you do:
Employee.add(type_name='boss')

Categories

Resources