如何在Sqlalchemy DataClass模式下避免最大递归深度超出错误
解决SQLAlchemy Dataclass双向关系JSON序列化无限递归问题
问题描述
将SQLAlchemy类映射为dataclass后,在Flask中导出JSON时,Child类会自动作为子列表在Parent类中导出,十分实用。但当想在Child类中显示父对象字段时,会触发maximum recursion depth exceeded无限递归错误。
疑问:是否可以在创建SQLAlchemy relationship时指定要选择的列?或者有没有其他方式自定义SQLAlchemy-dataclass对象的数据导出?
错误代码示例:
@reg.mapped_as_dataclass class Parent: __tablename__ = 'parent' Id: int = db.Column('id', db.Integer, primary_key=True) Name: str = db.Column('name', db.String) ChildrenList: 'Child' = db.relationship('Child', default_factory=list, back_populates='ParentObject') @reg.mapped_as_dataclass class Child: __tablename__ = 'child' Id: int = db.Column('id', db.Integer, primary_key=True) ParentId: int = db.Column('parent$id', db.Integer, ForeignKey('parent.id')) # 该字段引发无限递归 ParentObject: 'Parent' = db.relationship('Parent', back_populates='ChildrenList') # 期望实现的效果(SQLAlchemy原生不支持这类参数) # ParentObject: 'Parent' = db.relationship('Parent', back_populates='ChildrenList', select_fields='Id, Name') # ParentObject: 'Parent' = db.relationship('Parent', back_populates='ChildrenList', exclude_fields='ChildrenList') def json_return_example(): return db.session.query(Parent).all()
解决方案
方案1:自定义JSON编码器控制序列化字段
通过扩展Flask的JSON编码器,在序列化时跳过递归字段,或仅保留父对象的指定属性:
from flask import Flask, jsonify import json app = Flask(__name__) class CustomJSONEncoder(json.JSONEncoder): def default(self, obj): if hasattr(obj, '__dataclass_fields__'): data = {} for field in obj.__dataclass_fields__: if isinstance(obj, Child) and field == 'ParentObject': # 仅序列化父对象的Id和Name data['ParentObject'] = {'Id': obj.ParentObject.Id, 'Name': obj.ParentObject.Name} elif not (isinstance(obj, Parent) and field == 'ChildrenList'): data[field] = getattr(obj, field) return data return super().default(obj) app.json_encoder = CustomJSONEncoder # 接口示例 @app.route('/parents') def get_parents(): parents = db.session.query(Parent).all() return jsonify(parents)
方案2:用Hybrid Property返回父对象部分字段
将完整的双向关系隐藏为内部字段,通过混合属性返回需要的父对象数据,避免递归:
from sqlalchemy.ext.hybrid import hybrid_property @reg.mapped_as_dataclass class Child: __tablename__ = 'child' Id: int = db.Column('id', db.Integer, primary_key=True) ParentId: int = db.Column('parent$id', db.Integer, ForeignKey('parent.id')) # 内部关系字段,仅用于查询,不参与序列化 _ParentObject: 'Parent' = db.relationship('Parent', back_populates='ChildrenList') # 对外暴露的父对象数据,仅返回指定字段 @hybrid_property def ParentObject(self): return {'Id': self._ParentObject.Id, 'Name': self._ParentObject.Name} # Parent类需对应修改关系的back_populates @reg.mapped_as_dataclass class Parent: __tablename__ = 'parent' Id: int = db.Column('id', db.Integer, primary_key=True) Name: str = db.Column('name', db.String) ChildrenList: 'Child' = db.relationship('Child', default_factory=list, back_populates='_ParentObject')
方案3:使用Marshmallow定义序列化Schema
通过Marshmallow可以精确控制序列化字段,彻底规避递归问题:
首先安装依赖:pip install marshmallow
然后定义Schema:
from marshmallow import Schema, fields class ChildSchema(Schema): Id = fields.Int() ParentId = fields.Int() # 仅序列化父对象的指定字段 ParentObject = fields.Nested('ParentSchema', only=['Id', 'Name']) class ParentSchema(Schema): Id = fields.Int() Name = fields.Str() ChildrenList = fields.Nested(ChildSchema, many=True) # 接口示例 @app.route('/parents') def get_parents(): parents = db.session.query(Parent).all() schema = ParentSchema(many=True) return jsonify(schema.dump(parents))
补充说明
SQLAlchemy的relationship本身没有select_fields或exclude_fields这类参数,因为关系的核心作用是建立对象关联,而非直接控制序列化逻辑。解决递归问题的关键是在序列化阶段精准控制输出内容,或通过混合属性间接返回需要的父对象数据,避免加载完整的双向关联对象。
内容的提问来源于stack exchange,提问作者tehniss
相关产品推荐
相关产品推荐

