为什么 fields.Url('...') 根据渲染字段的数量在 Flask-RESTful 中中断

Why does fields.Url('...') break in Flask-RESTful based on the number of rendered fields

我有以下小 Flask-RESTful-based API:

from app import api, db, models
from flask import request
from flask.ext.restful import Resource, fields, marshal_with

foo_fields = {
    'id': fields.Integer,
    'name': fields.String,
    'self': fields.Url('.foo'),
    'created': fields.DateTime(dt_format='iso8601'),
    'updated': fields.DateTime(dt_format='iso8601'),
    'kind': fields.Raw('foo'),
}

class Foo(Resource):
    @marshal_with(foo_fields)
    def get(self, id):
        account = models.Foo.query.filter_by(id=id).first_or_404()
        return account

class FooList(Resource):
    @marshal_with(foo_fields)
    def get(self):
        accounts = models.Foo.query.order_by(models.Foo.id).all()
        return accounts

    @marshal_with(foo_fields)
    def post(self):
        json = request.get_json()
        foo = models.Foo()
        foo.name = json['name']
        db.session.add(foo)
        db.session.commit()
        return foo, 201

api.add_resource(Foo, '/foo/<int:id>', endpoint='foo')
api.add_resource(FooList, '/foo', endpoint='foo_list')

有一个使用 SQL Alchemy 支持的小模型,目前使用 SQLite 进行测试:

from app import app, db
from sqlalchemy.sql import func

class Foo(db.Model):
    __tablename__ = 'foo'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), index=True, unique=True)
    created = db.Column(db.DateTime, default=func.now())
    updated = db.Column(db.DateTime, default=func.now())

现在,我可以将其用于 POST 新对象,如下所示:

$ curl -v -X POST localhost:5000/api/v1/foo -H 'Content-Type: application/json' -d '{ "name": "foo" }'
{
    "created": "2015-10-21T12:39:41", 
    "id": 1, 
    "kind": "foo", 
    "name": "foo", 
    "self": "/api/v1/foo/1", 
    "updated": "2015-10-21T12:39:41"
}

但是,如果我减少渲染输出时使用的字段数量,那么当我 POST 另一个新对象时(并且仅当我 POST) 我收到以下错误:

BuildError: ('v1.foo', {'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x1054a3d10>}, None)

这只是由于编组字段中仍然有 fields.Url('.foo') 造成的,如果我也将其删除,那么一切都会重新开始。 GET 请求始终有效。

我还可以通过访问对象 post-db.session.commit() 中的字段之一来使事情正常进行,即 return 之前的 print foo.id

有人可以解释为什么这里的代码看起来有点脆弱吗?我可以看到,通过访问一个字段 post-commit 它可能会触发从数据库中读取。

我想我已经发现了问题,我认为这基本上归结为 Python 词典中键的自然排序顺序。

如果我禁用返回足够的键,则包含 fields.Url('.foo') 值的字段在遍历字典时将成为第一个键,因为 SQL Alchemy 的默认行为是在会话后使任何实例过期提交我在 POST 请求中创建的新对象暂时变成 None 这解释了尝试计算 URL 值时的错误。

访问任何其他字段,无论是在遍历字典的键时首先出现,还是我自己使用 print foo.id hack 都足以触发从数据库回读以刷新对象状态然后计算 URL 值按预期工作。

我想我可以通过在 SQL Alchemy 会话对象中设置 expire_on_commit=False 或使用 OrderedDict 并确保 URL 字段不是第一的。不过,我不确定前一种解决方案在副作用方面引入了什么。