-1

I am using Flask-Restplus and SQLAlchemy to develop my API. I want to return a response with information from two SQAlchemy objects, a user and a device, with a 1:1 relationship between them.

I have a query that looks like this:

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

For now, the result of the query above can be printed in console is like this:

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

I want my API endpoint to return the following JSON:

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

Here is my 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')
    })

Models for User and Device:

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 ..

I order to achieve JSON result above, I want the id and name is from the User object, and token is from the Device object.

Controller:

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)

Actual Response:

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

As you can see here, the same record appears 2 twice (once with token being null and once with token with the string I want).

How can I achieve the response that I want above?

ken
  • 1,406
  • 2
  • 25
  • 63
  • This is still lacking in clarity. What data is in the response, *just* the user device details? So `20` is the `id` for the `Device` object? That model doesn't *have* a `id` field, only your Restplus model has that field. You can [rename attributes](https://flask-restplus.readthedocs.io/en/stable/marshalling.html#renaming-attributes) but currently your examples *can't actually work*. – Martijn Pieters Feb 03 '19 at 21:53
  • Can you show us what attributes from what objects need to be reflected in the API? I did my best editing your question, but as it stands it is not clear if you want data from both objects or just the user device. – Martijn Pieters Feb 03 '19 at 21:54
  • `id` and `name` is from `user` object..and `token` is from `user_details` object – ken Feb 04 '19 at 00:37
  • bro I edited the question..let me know what u need again – ken Feb 04 '19 at 01:16
  • Is the `User.email` column perhaps meant to be `unique=True`? Can multiple users exist with the same email address? – Martijn Pieters Feb 04 '19 at 11:37
  • one email only can use by 1 user – ken Feb 04 '19 at 13:07
  • Then your response shouldn't really be a list, right? – Martijn Pieters Feb 04 '19 at 13:27

1 Answers1

2

You get two output entries because you have two objects (rows) in your query. You don't need to add the Device object to your query, because it is already available as the user.device. Adjust your Restplus model to use user.device.token attribute.

So you need to alter your query to only load the user:

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

If the User.email field is supposed to be unique (an email address should really reference just one user in your database), consider using .first() instead of .all(), then wrapping the return value of .first() in a list, or more logical, changing your return value into a single JSON object without a list. Also consider using the first_or_404() method to automatically produce a 404 Not Found response in case there is no user with this email.

You then need to configure your details API model:

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'
    )
})

Note the attribute field for token, we are telling Flask-Restplus here to take the token attribute of the device attribute of each object.

This does require you to alter your SQLAlchemy model, because you don't have a one-on-one relationship defined right now. You have a one to many, so user.device is actually a list. Add uselist=False to your device relationship:

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

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

That restricts you to a scalar value, so a single device object per user.

Or, if you did mean for it to be a one-to-many relationship, then you'll have to replace the token field with a tokens list:

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'
    )
})

You may want to rename device to devices in that case, to better reflect that you have multiple.

Note that in all cases, you'll want to think about loading strategies. If you are always going to access the device associated with the user, adding lazy="joined" to the device relationship is going to help performance. Alternatively, add .options(db.joinedload('device')) to your User query, before the .first() or .all() call:

return User.query.filter_by(email == data['email']).options(db.joinedload('device')).all()
Martijn Pieters
  • 889,049
  • 245
  • 3,507
  • 2,997
  • bro I modify the API model as u state,but the token is still null even though it exist in my table – ken Feb 04 '19 at 13:37
  • at the same time,I modify the query as u stated – ken Feb 04 '19 at 13:38
  • @ken: I see now you filtered on `Device` with `User.id == Device.id`. That's not your foreign key, so that's the wrong `Device` object you have there, the correct foreign key relationship is `User.id == Device.user_id`. For my query as `user = User.query.filter_by(email == data['email'])`, what is `user.device.token` set to? If that's `None`, then that's why you see that reflected in the response. – Martijn Pieters Feb 04 '19 at 13:41
  • @ken: there are more problems with the code in your question here that make it harder for me to help you. Your `Device` model in your question doesn't have a `id` field, it only has `user_device_id` as the primary key. – Martijn Pieters Feb 04 '19 at 13:41
  • opps...my bad.let me edit my question again..wait a minute – ken Feb 04 '19 at 13:44
  • sir,I edited my question.. the `id` in `device` class is the `foreignKey` from `User` object. – ken Feb 04 '19 at 13:46
  • can you take a look again for my `User` and `Device` class? now is exactly the same with my code now.. – ken Feb 04 '19 at 13:47
  • @ken: ah, I see. Your `User.device` attribute is a *list*, because you created a [one-to-many relationship](https://docs.sqlalchemy.org/en/latest/orm/basic_relationships.html#one-to-many). I'll be back in a bit to update my answer here, but you could try `attribute='device.0.token'` here to get just the single, first element. – Martijn Pieters Feb 04 '19 at 13:48
  • @ken: the correct solution is to use a [One to One mapping](https://docs.sqlalchemy.org/en/latest/orm/basic_relationships.html#one-to-one), so `device = db.relationship('Device', backref='user', uselist=False)` – Martijn Pieters Feb 04 '19 at 13:49
  • ok bro..I try for my own 1st..If possible please edit your answer as well..is really really appreciate..cause I total beginner on this..hahaha – ken Feb 04 '19 at 13:51