You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

升级SQLAlchemy 1.3至1.4后Hybrid Property失效,返回anon_1而非列名

问题

升级SQLAlchemy到1.4版本后,使用hybrid_property映射JSONB属性的功能失效。执行查询时,字典形式的结果无法获取正确列名,返回{'anon_1': 1881}而非预期的{'born': 1881}。相关代码如下:

import pytest
from sqlalchemy import Column, Integer, select
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.dialects.postgresql import JSONB

import os

from sqlalchemy import create_engine
from sqlalchemy.event import listens_for
from sqlalchemy.pool import StaticPool
from sqlalchemy.orm import Session

def create_postgres():
    engine = create_engine(os.environ["TEST_DATABASE"], poolclass=StaticPool)
    engine.execute("SET search_path TO pg_temp")

    @listens_for(engine, "engine_connect")
    def set_search_path(connection, branch):
        connection.execute("SET search_path TO pg_temp")

    return engine

@pytest.fixture
def session(request):
    engine = create_postgres()

    Base.metadata.create_all(engine)
    return Session(engine)


def update_json_property_names(attrs):
    for key, value in attrs.items():
        if isinstance(value, hybrid_property) and hasattr(value.fget, "__json_property"):
            setattr(value.fget, "prop_key", key)

def json_property(json_column, name, type_=None):
    def instance_get(self):
        return getattr(self, json_column.key)[name]

    def instance_set(self, value):
        json_obj = getattr(self, json_column.key)
        if json_obj is None:
            setattr(self, json_column.key, {})
        getattr(self, json_column.key)[name] = value

    def cls_get(cls):
        if type_:
            return json_column.cast(JSONB)[name].astext.cast(type_)
        else:
            return json_column.cast(JSONB)[name]

    prop = hybrid_property(
        fget=instance_get,
        expr=cls_get,
        fset=instance_set
    )

    prop.__json_property = (json_column.key, name)

    return prop


class ModelMetaType(DeclarativeMeta):
    def __new__(cls, name, bases, attrs):
        update_json_property_names(attrs)
        return DeclarativeMeta.__new__(cls, name, bases, attrs)



Base = declarative_base(metaclass=ModelMetaType)


class Author(Base):
    __tablename__ = "author"

    id = Column(Integer, primary_key=True)
    data = Column(JSONB, nullable=False)

    born = json_property(data, "yearBorn", Integer())
    died = json_property(data, "yearDied")

    def __repr__(self):
        return "Author(id={!r}, name={!r}, data={!r})".format(
            self.id, self.name, self.data
        )

def test_expression_has_name_of_json_property(session):
    session.add(Author(data={"yearBorn": 1881}))
    session.commit()
    q = select(Author.born)
    result = session.execute(q).one()
    assert result._asdict() == {"born": 1881}
解决方案

需要调整json_property的类级表达式生成逻辑,为查询结果指定明确的列别名,同时修改元类的属性绑定逻辑,确保别名与属性名一致。具体修改如下:

1. 修改json_property函数,为表达式添加别名

SQLAlchemy 1.4会为无别名的匿名表达式自动生成anon_*名称,因此需要在类级表达式中显式指定属性名作为别名。同时简化冗余的JSONB类型转换:

def json_property(json_column, name, type_=None):
    prop_key = None  # 后续由元类注入属性名

    def instance_get(self):
        return getattr(self, json_column.key)[name]

    def instance_set(self, value):
        json_obj = getattr(self, json_column.key)
        if json_obj is None:
            setattr(self, json_column.key, {})
        getattr(self, json_column.key)[name] = value

    def cls_get(cls):
        expr = json_column[name]  # 原json_column已是JSONB类型,无需重复cast
        if type_:
            expr = expr.astext.cast(type_)
        return expr.label(prop_key)  # 使用属性名作为列别名

    prop = hybrid_property(
        fget=instance_get,
        expr=cls_get,
        fset=instance_set
    )

    prop.__json_property = (json_column.key, name)
    prop.cls_get = cls_get  # 暴露cls_get,方便元类设置prop_key
    prop.cls_get.prop_key = prop_key

    return prop

2. 修改update_json_property_names函数,绑定属性名到表达式

在元类处理时,将属性名(如born)赋值给cls_get的prop_key变量,确保表达式生成时使用正确的别名:

def update_json_property_names(attrs):
    for key, value in attrs.items():
        if isinstance(value, hybrid_property) and hasattr(value, "__json_property"):
            value.cls_get.prop_key = key  # 将属性名注入到cls_get的prop_key中

3. 修正Author类的__repr__方法(可选)

原代码中__repr__引用了未定义的self.name,可以修正为:

def __repr__(self):
    return "Author(id={!r}, data={!r})".format(
        self.id, self.data
    )

修改后的完整测试代码

import pytest
from sqlalchemy import Column, Integer, select
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
from sqlalchemy.ext.hybrid import hybrid_property

import os

from sqlalchemy import create_engine
from sqlalchemy.event import listens_for
from sqlalchemy.pool import StaticPool
from sqlalchemy.orm import Session

def create_postgres():
    engine = create_engine(os.environ["TEST_DATABASE"], poolclass=StaticPool)
    engine.execute("SET search_path TO pg_temp")

    @listens_for(engine, "engine_connect")
    def set_search_path(connection, branch):
        connection.execute("SET search_path TO pg_temp")

    return engine

@pytest.fixture
def session(request):
    engine = create_postgres()

    Base.metadata.create_all(engine)
    return Session(engine)


def update_json_property_names(attrs):
    for key, value in attrs.items():
        if isinstance(value, hybrid_property) and hasattr(value, "__json_property"):
            value.cls_get.prop_key = key

def json_property(json_column, name, type_=None):
    prop_key = None

    def instance_get(self):
        return getattr(self, json_column.key)[name]

    def instance_set(self, value):
        json_obj = getattr(self, json_column.key)
        if json_obj is None:
            setattr(self, json_column.key, {})
        getattr(self, json_column.key)[name] = value

    def cls_get(cls):
        expr = json_column[name]
        if type_:
            expr = expr.astext.cast(type_)
        return expr.label(prop_key)

    prop = hybrid_property(
        fget=instance_get,
        expr=cls_get,
        fset=instance_set
    )

    prop.__json_property = (json_column.key, name)
    prop.cls_get = cls_get
    prop.cls_get.prop_key = prop_key

    return prop


class ModelMetaType(DeclarativeMeta):
    def __new__(cls, name, bases, attrs):
        update_json_property_names(attrs)
        return DeclarativeMeta.__new__(cls, name, bases, attrs)



Base = declarative_base(metaclass=ModelMetaType)


class Author(Base):
    __tablename__ = "author"

    id = Column(Integer, primary_key=True)
    data = Column(JSONB, nullable=False)

    born = json_property(data, "yearBorn", Integer())
    died = json_property(data, "yearDied")

    def __repr__(self):
        return "Author(id={!r}, data={!r})".format(
            self.id, self.data
        )

def test_expression_has_name_of_json_property(session):
    session.add(Author(data={"yearBorn": 1881}))
    session.commit()
    q = select(Author.born)
    result = session.execute(q).one()
    assert result._asdict() == {"born": 1881}

原理说明

SQLAlchemy 1.4对查询结果的列名解析逻辑更严格,未指定别名的表达式会被自动命名为anon_*。通过在类级表达式中调用.label(prop_key),并将属性名绑定到prop_key,我们强制将查询结果的列名设置为属性名称,从而让_asdict()返回预期的键值对。

内容的提问来源于stack exchange,提问作者Andrew

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 13:21:28