Flask-Restplus 如何将来自 2 table 的数据组合成单个数据响应?

Flask-Restplus how to combine data from 2 table into single data response?

我正在使用 Flask-Restplus 和 SQLAlchemy 来开发我的 API。我想要 return 一个响应,其中包含来自两个 SQAlchemy 对象(一个用户和一个设备)的信息,它们之间具有 1:1 关系。

我有一个如下所示的查询:

details = db.session.query(User, Device).filter(User.id == Device.id) \
                    .filter(User.email== data['email'])\
                    .all()

目前,上面查询的结果可以在控制台打印如下:

[(<User 'None'>, <Device 1>)]

我希望我的 API 端点到 return 以下 JSON:

{
  "data": [
    [
      {
        "id": 20,
        "name": null,
        "token": "Some String here"
      }
    ]
  ]
}

这是我的 DTO:

class UserDto:
    # this is for input
    user = api.model('user', {
        'email': fields.String(required=False, description='phone number'),
        'name': fields.String(required=False, description='username'),
        'device_id': fields.String(required=False,description='user_device_id'),
   })

   # this is for output
    details = api.model('details', {
        'id': fields.Integer(required=False, description='the id'),
        'name': fields.String(required=False, description='name'),
        'token': fields.String(required=False, description='token')
    })

UserDevice 的模型:

class User(db.Model):
    __tablename__ = "users_info"

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.Integer, unique=True, nullable=True)
    email = db.Column(db.String)
    device = db.relationship('Device', backref='user')
    # .. more fields ..


class Device(db.Model):
    __tablename__ = "user_device"

    user_device_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    id = db.Column(db.Integer, db.ForeignKey(User.id))
    token = db.Column(db.String, nullable=True)
    # .. more fields ..

我为了实现上面的 JSON 结果,我想要 idname 来自 User 对象,而 token 来自Device 对象。

控制器:

api = UserDto.api
_user = UserDto.user
_details = UserDto.details

@api.route('/')
class User(Resource):
    @api.response(201, 'successful')
    @api.expect(_user, validate=True)
    @api.marshal_list_with(_details, envelope='data')
    def post(self):
        data = request.json
        return user(data=data)

实际响应:

{
  "data": [
    [
      {
        "id": 20,
        "name": null,
        "token": null
      },
      {
        "id": 20,
        "name": null,
        "token": "some string here"
      }
    ]
  ]
}

正如您在此处看到的,同一条记录出现了 2 次(一次 tokennull,一次 token 为我想要的字符串)。

怎样才能达到我想要的上面的响应?

您得到两个输出条目,因为您的查询中有两个对象(行)。您不需要将 Device 对象添加到您的查询中,因为它已经作为 user.device 可用。调整您的 Restplus 模型以使用 user.device.token 属性。

因此您需要更改查询以仅加载用户:

return User.query.filter_by(email == data['email']).all()

如果 User.email 字段应该是唯一的(电子邮件地址实际上应该只引用数据库中的一个用户),请考虑使用 .first() 而不是 .all(),然后换行列表中 .first() 的 return 值,或更合乎逻辑,将 return 值更改为单个 JSON 对象,而无需列表。还可以考虑使用 first_or_404() method 自动生成 404 Not Found 响应,以防没有用户使用此电子邮件。

然后您需要配置您的 details API 型号:

details = api.model('details', {
    'id': fields.Integer(required=False, description='the id'),
    'name': fields.String(required=False, description='name'),
    'token': fields.String(
        attribute='device.token',
        required=False,
        description='token'
    )
})

注意tokenattribute字段,我们在这里告诉Flask-Restplus取每个对象的device属性的token属性。

这确实需要您更改 SQLAlchemy 模型,因为您现在没有定义一对一关系。您与 device 的关系是 one to many, so user.device is actually a list. Add uselist=False

class User(db.Model):
    __tablename__ = "users_info"

    # ...
    device = db.relationship('Device', backref='user', uselist=False)

这将您限制为 标量 值,因此每个用户一个 device 对象。

或者,如果您的意思是它是一对多关系,那么您必须将 token 字段替换为 tokens 列表:

details = api.model('details', {
    'id': fields.Integer(required=False, description='the id'),
    'name': fields.String(required=False, description='name'),
    'token': fields.List(
        fields.String(attribute='token', required=False),
        description='array of tokens for all user devices',
        attribute='device'
    )
})

在这种情况下,您可能希望将 device 重命名为 devices,以更好地反映您有多个。

请注意,在所有情况下,您都需要考虑 loading strategies。如果您总是要访问与用户关联的设备,将 lazy="joined" 添加到 device 关系将有助于提高性能。或者,在 .first().all() 调用之前将 .options(db.joinedload('device')) 添加到您的 User 查询:

return User.query.filter_by(email == data['email']).options(db.joinedload('device')).all()