为什么 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 字段不是第一的。不过,我不确定前一种解决方案在副作用方面引入了什么。
我有以下小 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 字段不是第一的。不过,我不确定前一种解决方案在副作用方面引入了什么。