SQLAlchemy hybrid_property查询报错:relativedelta仅支持日期类型
我定义了一个包含age混合属性的SQLAlchemy模型类,实例调用时能正确计算年龄,但用这个属性做查询过滤时却报错了。
我的模型代码(简化版)
class Persons(Base): __tablename__ = 'm_persons' birthDate = db.Column(db.Date, nullable=True) @hybrid_property def age(self): bd = self.birthDate return relativedelta(date.today(), bd).years @age.expression def age(cls): bd = cls.birthDate return relativedelta(date.today(), bd).years
实例调用正常
person = Person.query.get(1) print(person.age) # 能正确输出年龄
查询时触发报错
当我尝试用age属性做过滤查询:
Persons.query.filter(Persons.age >= min_age).all()
得到报错:
TypeError: relativedelta only diffs datetime/date
我知道这是因为查询时cls.birthDate是sqlalchemy.orm.attributes.InstrumentedAttribute类型,不是Python的date对象,所以relativedelta无法处理。请问该怎么修改才能让这个属性适配查询场景?
补充报错栈信息
File "/Users/ext334/Dev/python/asocio/app/members/forms.py", line 174, in __init__ self.person.choices = listPersons(gender=subscription.gender, min_age=subscription.min_age, max_age=subscription.max_age) File "/Users/ext334/Dev/python/asocio/app/members/forms.py", line 31, in listPersons print(Persons.query.join(Members).filter(Persons.age >= min_age, Persons.age <= max_age, extract('year',Persons.birthDate) >= minBirthYear, extract('year',Persons.birthDate) <= maxBirthYear, Persons.sex == gender).order_by(Members.lastname, Members.firstname)) File "/Users/ext334/Dev/python/venv36/lib/python3.6/site-packages/sqlalchemy/ext/hybrid.py", line 867, in __get__ return self._expr_comparator(owner) File "/Users/ext334/Dev/python/venv36/lib/python3.6/site-packages/sqlalchemy/ext/hybrid.py", line 1066, in expr_comparator owner, self.__name__, self, comparator(owner), File "/Users/ext334/Dev/python/venv36/lib/python3.6/site-packages/sqlalchemy/ext/hybrid.py", line 1055, in _expr return ExprComparator(cls, expr(cls), self) File "/Users/ext334/Dev/python/asocio/app/members/models.py", line 235, in age return relativedelta(date.today(), cls.birthDate).years File "/Users/ext334/Dev/python/venv36/lib/python3.6/site-packages/dateutil/relativedelta.py", line 102, in __init__ raise TypeError("relativedelta only diffs datetime/date") TypeError: relativedelta only diffs datetime/date
完整查询语句
Persons.query.join(Members).filter( Persons.age >= min_age, Persons.age <= max_age, extract('year',Persons.birthDate) >= minBirthYear, extract('year',Persons.birthDate) <= maxBirthYear, Persons.sex == gender ).order_by(Members.lastname, Members.firstname)
完整Persons类
class Persons(Base): """ Description of a person """ __tablename__ = 'm_persons' user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE', onupdate='CASCADE')) family_id = db.Column(db.Integer, db.ForeignKey('m_families.id', ondelete='CASCADE', onupdate='CASCADE')) birthDate = db.Column(db.Date, nullable=True) sex = db.Column(db.String(1), nullable=False, default='U') # U = unknown, F = Female, M = Male # return the age in years, not rounded @hybrid_property def age(self): return relativedelta(date.today(), self.birthDate).years @age.expression def age(cls): return relativedelta(date.today(), cls.birthDate).years
问题核心很明确:你在@age.expression里直接用了Python的relativedelta,但这个函数只认Python原生的date/datetime对象。而当你用Persons.age做查询时,cls.birthDate是SQLAlchemy生成的字段表达式(不是实际的日期值),relativedelta根本处理不了它,自然就报错了。
咱们得把年龄计算改成数据库能看懂的SQL表达式,让数据库自己去计算年龄,而不是在Python里算。不同数据库的日期函数有点不一样,给你两种常用方案:
方案1:PostgreSQL专属(最简单)
PostgreSQL自带AGE()函数,能直接计算两个日期的年龄差,咱们只要提取年份就行:
from sqlalchemy import func @age.expression def age(cls): # 用数据库的AGE函数计算当前日期和生日的差,再提取年份 return func.extract('year', func.age(func.current_date(), cls.birthDate))
方案2:通用兼容版(支持MySQL、SQLite等)
如果你的项目要适配多种数据库,就用日期差的逻辑手动计算,还要处理“生日还没到”的情况(比如今天3月10号,用户生日4月1号,今年的生日还没过,年龄得减1):
from sqlalchemy import func, case @age.expression def age(cls): today = func.current_date() # 先算年份差 year_diff = func.extract('year', today) - func.extract('year', cls.birthDate) # 判断今年生日是否已经过了,没到的话年龄减1 return case( [ ( # 把日期截断到年份,比较今年的今天和生日的年份部分 func.date_trunc('year', today) <= func.date_trunc('year', cls.birthDate), year_diff - 1 ) ], else_=year_diff )
额外提示
如果birthDate是NULL(用户没填生日),这两种方案都会返回NULL,你可以根据业务需求在hybrid_property和expression里加判断,比如返回None或者0:
# 给hybrid_property加判断 @hybrid_property def age(self): if not self.birthDate: return None return relativedelta(date.today(), self.birthDate).years # 给expression加判断(以通用版为例) @age.expression def age(cls): today = func.current_date() year_diff = func.extract('year', today) - func.extract('year', cls.birthDate) age_val = case( [ (func.date_trunc('year', today) <= func.date_trunc('year', cls.birthDate), year_diff - 1) ], else_=year_diff ) # 如果birthDate为NULL,返回NULL或者你想要的默认值 return case( [(cls.birthDate.is_(None), None)], else_=age_val )
这样修改后,不管是实例调用person.age还是用Persons.age做查询过滤,都能正常工作了。
内容的提问来源于stack exchange,提问作者Pierre

