sqlalchemy的hybrid_property和TypeDecorator一起使用,让属性飞起来
🤖 Assistant
如果希望一个属性既是一个混合属性(在 Python 和 SQL 层面都有动态行为),同时又对应一个实际的数据库列,那么这个混合属性的 expression 部分就应该返回那个实际的数据库列。
正确的实现方式
如果你希望 ids 属性在 Python 端由 getter/setter 控制,并且它的值还能够持久化到数据库中,那么你应该将它定义为一个 hybrid_property,并且这个 hybrid_property 内部关联到一个另外命名的实际数据库列。
以下是正确的实现方式:
from sqlalchemy import Column, TEXT, Integer
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from sqlalchemy.types import TypeDecorator # 确保导入 TypeDecorator
# 1. 自定义 TypeDecorator
class ListAsText(TypeDecorator):
 impl = TEXT
 cache_ok = True # 提高性能,表示该类型可以被缓存
 def process_bind_param(self, value, dialect):
 print(f"--- Calling process_bind_param for {value} ---")
 if value is None:
 return value
 # 假设 value 是一个列表,将其转换为字符串存储
 return ','.join(map(str, value))
 def process_result_value(self, value, dialect):
 print(f"--- Calling process_result_value for {value} ---")
 if value is None:
 return value
 # 假设 value 是一个逗号分隔的字符串,将其转换为列表
 return [int(x) for x in value.split(',') if x] # 或者其他类型转换
Base = declarative_base()
# 2. 模型定义
class MyClass(Base):
 __tablename__ = 'my_table'
 id = Column(Integer, primary_key=True)
 # 实际存储在数据库中的列。我们给它一个不同的、内部使用的名字
 _ids_stored = Column("ids", ListAsText) # 注意这里设置列名为 "ids"
 @hybrid_property
 def ids(self):
 print("--- Calling ids_getter ---")
 # Python getter: 返回存储值的解包形式
 return self._ids_stored
 @ids.setter
 def ids(self, value):
 print(f"--- Calling ids_setter with value: {value} ---")
 # Python setter: 将传入的值设置给实际存储的列
 self._ids_stored = value
 @ids.expression
 def ids(cls):
 # SQL expression: 当在查询中使用 'ids' 时,实际引用的是 _ids_stored 列
 # 这样才能在SQL层面过滤或排序这个字段
 return cls._ids_stored
# --- 测试部分 ---
engine = create_engine('sqlite:///:memory:', echo=True)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
# 创建一个实例
obj = MyClass()
print("\n--- Assigning obj.ids ---")
obj.ids = [1, 2, 3] # 调用 ids_setter
print(f"Object's _ids_stored after set: {obj._ids_stored}")
# 将对象添加到会话并提交 (触发 process_bind_param)
print("\n--- Committing to DB ---")
session.add(obj)
session.commit()
# 清除会话,重新加载以模拟从数据库读取 (触发 process_result_value)
session.close()
session = Session()
print("\n--- Loading obj from DB ---")
loaded_obj = session.query(MyClass).first()
print(f"Loaded obj.ids (getter): {loaded_obj.ids}") # 调用 ids_getter 和 process_result_value
# 数据库查询使用 hybrid_property (调用 SQL expression)
print("\n--- Querying using hybrid_property ---")
# 注意:这里我们查询的是 obj._ids_stored,因为ListAsText在绑定时转成了字符串
# 所以查询时也要用字符串匹配
filtered_obj = session.query(MyClass).filter(MyClass.ids.like('%2,3%')).first()
if filtered_obj:
 print(f"Filtered obj found: {filtered_obj.id}, ids: {filtered_obj.ids}")
else:
 print("No object found by filter.")
session.close()
解释正确代码如何达成期望效果
- _ids_stored = Column("ids", ListAsText):
- 这是一个实际的 ORM 列,名为 _ids_stored(为了避免与hybrid_property冲突)。
- "ids"是数据库中实际的列名。
- 它使用了你的 ListAsText自定义类型。这意味着所有与数据库的交互(绑定参数、处理结果)都将通过ListAsText的方法。
- @hybrid_property; def ids(self): ...:
- 这个装饰器创建了一个名为 ids的混合属性,它是我们希望在业务代码中使用的名称。
- getter(- def ids(self): return self._ids_stored): 当你访问- obj.ids时,它会调用这个 getter,并返回- _ids_stored(这个值在从数据库加载后,已经经过- process_result_value处理成列表了)。
- setter(- @ids.setter; def ids(self, value): self._ids_stored = value): 当你设置- obj.ids = some时,它会调用这个 setter,将- some值直接赋值给- _ids_stored。
- @ids.expression; def ids(cls): return cls._ids_stored:
- 这个 expression函数定义了当ids属性用于 SQL 查询时,应该如何生成 SQL 表达式。
- 它返回 cls._ids_stored,这意味着在 SQL 查询中,MyClass.ids会被翻译成对数据库中ids列的引用。
总结
通过将实际的数据库列命名为内部使用的名字(如 _ids_stored),并将 hybrid_property 命名为业务逻辑中使用的名字(如 ids),并在 hybrid_property 的 getter、setter 和 expression 中正确地引用这个内部列,你就能实现所有期望的效果:
- Python getter/setter:由 hybrid_property控制。
- 数据库读写(TypeDecorator):由 Column(ListAsText)及其process_bind_param/process_result_value方法控制。
- SQL 查询(Hybrid Expression):由 hybrid_property.expression控制,使其能正确引用底层数据库列。
 
                    
                 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号