Query One to Many Relationship SQLAlchemy - python

I am trying to query the users based upon their skills from these tables.
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(64), unique=True, index=True)
username = db.Column(db.String(64), unique=True, index=True)
skills = db.relationship('Skill', backref='author', lazy='dynamic')
class Skill(db.Model):
__tablename__ = 'skills'
id = db.Column(db.Integer, primary_key=True)
skill = db.Column(db.String(64), index=True)
author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
i tried this in User table and got this error.
#classmethod
def users_by_skill(cls, skill):
return User.query.join(Skill).filter(skill.skill).all()
AttributeError: 'str' object has no attribute 'skill'
Where i am missing badly?

You define the following class method:
#classmethod
def users_by_skill(cls, skill):
return User.query.join(Skill).filter(skill.skill).all()
You are probably expecting to use this function like so:
users = Users.users_by_skill('nunchuk')
That means the skill argument in users_by_skill is a string. Then, you try to use skill.skill, which essentially is like doing 'nunchuk'.skill. Python does not have a skill attribute on the string class, hence the error.
The filter function actually takes a Criteria object. In other words, you don't pass it a value like "filter", you instead pass it a criterion that represents the notion of "the skill column on the Skill table must equal 'nunchuk'". You can do this using syntax like the following:
#classmethod
def users_by_skill(cls, skill_name):
return User.query.join(Skill).filter(Skill.skill == skill_name).all()

Related

How to correctly define relationships in a Python SQLAlchemy class with multiple base classes?

I'm trying to recreate my database using SQLAlchemy and Flask.
I've created all the models, but now have some problems with the relationships between the models. When inserting or updating an object Flask returns the following error message:
File "C:\Users\Lenna\SchoolMi\api-server-v4\venv\lib\site-packages\sqlalchemy\ext\declarative\clsregistry.py", line 326, in __call__
x = eval(self.arg, globals(), self._dict)
File "<string>", line 1, in <module>
# ext/declarative/clsregistry.py
AttributeError: 'Table' object has no attribute 'id'
The error message references to the active_channel relationship in the profile class and indicates that the channel class has no id attribute. However I've already defined this attribute in the ObjectWithDefaultProps class. After inspection of the SQL file in a SQL browser, the id attribute is indeed present on the Channel entity.
My first assumption was a misconfiguration of the foreignkey, so I tried to change the foreignkey:
active_channel = db.relationship("Channel", foreign_keys="Channel.id")
instead of
active_channel = db.relationship("Channel", foreign_keys="channel.id")
Unfortunately, this did not work. The error changes to the following:
sqlalchemy.exc.NoForeignKeysError: Could not determine join condition between parent/child tables on
relationship Profile.active_channel - there are no foreign keys linking these tables. Ensure that referencing
columns are associated with a ForeignKey or ForeignKeyConstraint, or specify a 'primaryjoin' expression. //
Werkzeug Debugger
I've looked further into the polymorphism aspects of SQLAlchemy and thought it might had something to do with the mapperargs but couldn't figure out the correct way of implementing this.
In my code I have the following classes:
My entities derive from multiple objects, that represent shared attributes or relationships.
class Profile(db.Model, ObjectWithDefaultProps, ObjectWithAvatar, ObjectWithNotificationProfile):
__tablename__ = "profile"
firebase_uid = db.Column(db.String, unique=True, nullable=False, primary_key=True)
username = db.Column(db.String, unique=True, nullable=False)
firstname = db.Column(db.String, nullable=False)
lastname = db.Column(db.String, nullable=False)
about = db.Column(db.String)
score = db.Column(db.Integer)
email = db.Column(db.String, nullable=False)
active_channel_id = db.Column(db.Integer, db.ForeignKey("channel.id"))
active_channel = db.relationship("Channel", foreign_keys="channel.id")
Channel.py
class Channel(db.Model, ObjectBase, ObjectWithAvatar, ObjectWithName, ProfileLinkedObject):
__tablename__ = "channel"
description = db.Column(db.String)
can_add_tags = db.Column(db.Boolean, default=False, nullable=False)
can_public_join = db.Column(db.Boolean, default=False, nullable=False)
from database.provider import db
from datetime import datetime
class ObjectWithDefaultProps:
deleted = db.Column(db.Boolean, nullable=False, default=False)
date_modified = db.Column(db.DateTime, default=datetime.utcnow)
date_added = db.Column(db.DateTime, onupdate=datetime.utcnow)
from database.provider import db
from database.extensions.object_with_color import ObjectWithColor
class ObjectWithAvatar(ObjectWithColor):
image_url = db.Column(db.String)
from database.provider import db
class ObjectWithColor:
color_index = db.Column(db.Integer, default=0)
from database.provider import db
from sqlalchemy.ext.declarative import declared_attr
class ObjectWithNotificationProfile:
auto_follow_questions = db.Column(db.Integer)
auto_follow_answers = db.Column(db.Integer)
auto_follow_comments = db.Column(db.Integer)
auto_follow_questions_on_comment = db.Column(db.Integer)
auto_follow_questions_on_answer = db.Column(db.Integer)
auto_follow_answers_on_comment = db.Column(db.Integer)
send_new_data_notification = db.Column(db.Boolean)
send_new_members_notification = db.Column(db.Boolean)
#declared_attr
def question_event_preferences_id(cls):
return db.Column(db.Integer, db.ForeignKey("custom_event_preferences.id"))
#declared_attr
def question_event_preferences(cls):
return db.relationship("CustomEventPreferences", foreign_keys="custom_event_preferences.id")
#declared_attr
def answer_event_preferences_id(cls):
return db.Column(db.Integer, db.ForeignKey("custom_event_preferences.id"))
#declared_attr
def answer_event_preferences(cls):
return db.relationship("CustomEventPreferences", foreign_keys="custom_event_preferences.id")
#declared_attr
def comment_event_preferences_id(cls):
return db.Column(db.Integer, db.ForeignKey("custom_event_preferences.id"))
#declared_attr
def comment_event_preferences(cls):
return db.relationship("CustomEventPreferences", foreign_keys="custom_event_preferences.id")
#declared_attr
def question_tagging_preferences_id(cls):
return db.Column(db.Integer, db.ForeignKey("custom_tagging_preferences.id"))
#declared_attr
def question_tagging_preferences(cls):
return db.relationship("CustomTaggingPreferences", foreign_keys="custom_tagging_preferences.id")
#declared_attr
def answer_tagging_preferences_id(cls):
return db.Column(db.Integer, db.ForeignKey("custom_tagging_preferences.id"))
#declared_attr
def answer_tagging_preferences(cls):
return db.relationship("CustomTaggingPreferences", foreign_keys="custom_tagging_preferences.id")
#declared_attr
def comment_tagging_preferences_id(cls):
return db.Column(db.Integer, db.ForeignKey("custom_tagging_preferences.id"))
#declared_attr
def comment_tagging_preferences(cls):
return db.relationship("CustomTaggingPreferences", foreign_keys="custom_tagging_preferences.id")
For those facing the same or similar issues as described in my question:
I've managed to get my code working by replacing the string variables in the foreign_keys attribute with the column variables itself.
After testing, I found that using a string as foreign_keys attribute always trigged an error for me, but the alternative methods using primaryjoin and foreign_keys=cls.foreign_key worked.
#declared_attr
def comment_event_preferences(cls):
return db.relationship("CustomEventPreferences", foreign_keys="custom_event_preferences.id")
becomes:
#declared_attr
def comment_event_preferences(cls):
return db.relationship("CustomEventPreferences", foreign_keys=cls.comment_event_preferences)

Flask / SQLAlchemy - Request tables with many-to-many relationships

I used Flask and SQLAlchemy to create an application based on a database. Here is the classes that I have defined:
models.py
class HasTag(db.Model):
tagged_document_id = db.Column(db.Integer, db.ForeignKey('Document.id'), primary_key=True)
document_tag_id = db.Column(db.Integer, db.ForeignKey('Tag.id'), primary_key=True)
class Document(db.Model):
id = db.Column(db.Integer, unique=True, nullable=False, primary_key=True, autoincrement=True)
title = db.Column(db.Text)
tag = db.relationship("Tag",
secondary=HasTag,
back_populates="tagged_document",
lazy="dynamic")
class Tag(db.Model):
id = db.Column(db.Integer, unique=True, nullable=False, primary_key=True, autoincrement=True)
label = db.Column(db.String, nullable=False)
tagged_document = db.relationship("Document",
secondary=HasTag,
back_populates="tag",
lazy="dynamic")
In the application, I have an advanced search form where it is possible to do a full text search through the different fields of the Document table.
routes.py
#app.route("/search")
def search():
keyword = request.args.get("keyword", None)
query = Document.query
if keyword:
query = Document.query.filter(or_(
Document.title.like("%{}%".format(keyword)),
...
))
The thing is, I'd like to be able to search the keyword given by the user also in the label of the tag. I tried something like:
if keyword:
query = Document.query.join(Tag).filter(or_(
Document.title.like("%{}%".format(keyword)),
...,
Tag.label.like("%{}%".format(keyword))
))
But I get this error: AttributeError: 'HasTag' object has no attribute 'foreign_keys'
Can you help me? Thanks!
I have a similar structure in one of my projects, and this is how I define relatioship:
leagues = db.relationship("League",
secondary=LeagueTeamAssociation.__tablename__,
back_populates="teams")
So, You need to provide table name to secondary parameter, either using above syntax (You'll need to add __tablename__ to your HasTag class) or using string "has_tag" (provided that this is the name of the table in the database).

"has_many :through" construct in sqlalchemy

In rails we can simply define relationships with the has_many :through syntax in order to access 2nd, 3rd .. nth degree relations.
In SQLAlchemy however, this seems to be more difficult. I'm trying to avoid going down the route of writing joins, as I find them to be anti-patterns in trying to keep a clean code base.
My tables look like following:
class Message(db.Model):
__tablename__ = 'message'
id = db.Column(db.Integer, primary_key=True)
text = db.Column(db.String())
user_id = db.Column(db.ForeignKey("user.id"))
user = db.relationship('User', backref="messages")
class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String())
class Level(db.Model):
__tablename__ = 'level'
number = db.Column(db.Integer, nullable=False, primary_key=True)
name = db.Column(db.String(), nullable=False, primary_key=True)
users = db.relationship(
"User",
secondary="user_level",
backref="levels")
class UserLevel(db.Model):
__tablename__ = 'user_level'
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
number = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(), primary_key=True)
__table_args__ = (
db.ForeignKeyConstraint(
['number', 'name'],
['level.number', 'level.name']
),
)
The idea is that a user can have multiple authorisation levels (e.g. a user can be at level 1, 3 and 6 at the same time). As the data I have does not contain unique sequence numbers for available levels, I had to resort to the use of composite keys to keep the data consistent with future updates.
To get all messages for a level I can currently do something like this:
users = Level.query[0].users
for user in users:
results.append(user.messages)
return results
This gives me all users on a level. But in order to get all messages for a certain level, I have to loop through these users and append them to a results list.
What I'd like to do is:
return Level.query[0].users.messages
This is more like the syntax I am used to from rails. How would one accomplish this in flask-SQLAlchemy?

When do relationships / backrefs become usable

I'm having a difficult time understanding how relationships / backrefs work.
I seem to be missing the point regarding how to make them 'live' so I keep getting errors like:
'NoneType' object has no attribute 'decid'.
These tables are 1 to 1 forming a heirachy.
I have an SQLite db and the following classes defined.
class Person(DECLARATIVE_BASE):
__tablename__ = 'person'
__table_args__ = ({'sqlite_autoincrement': True})
idperson = Column(INTEGER, autoincrement=True,
primary_key=True, nullable=False)
lastname = Column(VARCHAR(45), index=True, nullable=False)
firstname = Column(VARCHAR(45), index=True, nullable=False)
def __repr__(self):
return self.__str__()
def __str__(self):
return "<Person(%(idperson)s)>" % self.__dict__
class Schoolmember(DECLARATIVE_BASE):
__tablename__ = 'schoolmember'
person_id = Column(INTEGER, ForeignKey("person.idperson"),
index=True, primary_key=True, nullable=False)
decid = Column(VARCHAR(45), unique=True, nullable=False)
type = Column(VARCHAR(20), nullable=False)
person = relationship("Person", foreign_keys=[person_id],
backref=backref("schoolmember", uselist=False))
def __repr__(self):
return self.__str__()
def __str__(self):
return "<Schoolmember(%(person_id)s)>" % self.__dict__
class Student(DECLARATIVE_BASE):
__tablename__ = 'student'
person_id = Column(INTEGER, ForeignKey("schoolmember.person_id"),
autoincrement=False, primary_key=True, nullable=False)
studentnum = Column(VARCHAR(30), unique=True, nullable=False)
year = Column(INTEGER, nullable=False)
graduated = Column(BOOLEAN, default=0, nullable=False)
schoolmember = relationship("Schoolmember", foreign_keys=[person_id],
backref=backref("student", uselist=False))
def __repr__(self):
return self.__str__()
def __str__(self):
return "<Student(%(person_id)s)>" % self.__dict__
I don't understand why here I can't access schoolmember from Student.
I was expecting declaritive to cascade up the relationships.
newstu = Student()
newstu.studentnum = '3456'
newstu.schoolmember.decid = 'fred.frog' # Error, 'NoneType' object
The following works, but only by stomping over the relationships defined in the class?
Do I need to do it this way?
s = Schoolmember(decid = 'fred.frog')
newstu = Student(schoolmember=s, studentnum='3456')
I don't 'get' what's is going on. I'm trying to understand the principals involved so I don't get bamboozled by the next problem
The reason your first example doesn't work is because when you initialize student there is no schoolmember associated with it. SQLAlchemy doesn't automatically generate this for you. If you wanted to, every time you create a Student object for it to automatically create a new schoolmember, you could do that inside of an __init__. In addition, if you wanted it to work you could do something like:
student = Student()
schoolmember = Schoolmember()
student.studentnum = 3456
student.schoolmember = schoolmember
student.schoolmember.decid = 'fred.frog'
An __init__ method could help also, if this is behavior you want every time.
def __init__(self, studentnum=None, year=None, graduated=None, schoolmember=None):
# If no schooolmember was provided, automatically generate one.
self.schoolmember = schoolmember or Schoolmember()
Hope this helps.

How can I use autoincrementing column value as default value for another not nullable column?

Lets say that we have UserModel with following columns:
class UserModel(declarative_base(bind=engine)):
__tablename__ = "users"
id = Column(Integer, autoincrement=True, primary_key=True)
nickname = Column(String, unique=True, nullable=False)
Is there a way to set id as default value for nickname without changing anything outside the model class?
I tried using default=id and server_default=id, but IntegrityError is always raised on commit.
Also, I know that there is no id until commit or flush is performed, but calling flush outside is not an option for me.
Thanks.
You should use hybrid_property for this. Here example:
class UserModel(Base):
__tablename__ = "users"
id = Column(Integer, autoincrement=True, primary_key=True)
nickname_str = Column(String, unique=True)
#hybrid_property
def nickname(self):
return self.nickname_str or str(self.id)
#nickname.expression
def nickname(self):
return case(
[(self.nickname_str != None, self.nickname_str)],
else_=cast(self.id, String)
).label('nickname')
#nickname.setter
def nickname(self, value):
self.nickname_str = value
Full example here.

Categories