flask-smorest icon indicating copy to clipboard operation
flask-smorest copied to clipboard

Query parameter for sorting response

Open kettenbach-it opened this issue 5 years ago • 11 comments

Is there a native way to add a "sort" parameter to each QuerySchema to determine the order of the result set? With native I mean: is that part of smorest or webargs etc. If not: how could it be done in a generic way without modifying all my controllers.

kettenbach-it avatar Jan 13 '20 12:01 kettenbach-it

I don't see how this could be done in a generic way. It depends on the way the framework processes queries.

I generally create a SortSchema, that inherits from my BaseSchema and contains a sort String field. And I use this as a base schema for GET queries. This at least allows me to handle a sort argument.

Currently, I don't validate that the string received corresponds to a field in the database. Doing this validation would require either more manual inputs or tighter integration with the database ORM/ODM. Or more logic to make this depend on the input Schema. If the string does not correspond to a field, it is silently ignored, which is not ideal.

I also added some custom processing to be able to receive strings such as "sort=-age,+state" and translate it into MongoDB requests (for instance).

I'm open to ideas about how to improve flask-smorest to provide helpers in a generic way.

lafrech avatar Jan 13 '20 13:01 lafrech

I think your suggesting is pretty much what I need. Even better would be to have it in the smorest package, but I unterstand that this highly dependent on the underlying database.

One follow-up question: how do you handle the sort argument in your controller?

The way I handle the query arguments as well as the pagination_parameters is like that:

@classmethod
@MyBlp.arguments(MyQuerySchema, location="query")
@MyBlp.response(MySchema(many=True))
@MyBlp.paginate()
def get(cls, args, pagination_parameters):
    query = Mymodel.query.filter_by(**args)
    pagination_parameters.item_count = query.count()
    return query.paginate(pagination_parameters.page, pagination_parameters.page_size, True).items

(I run sqlalchemy with MySQL on the background)

If I understand it correctly, the sortfield would be part of the args parameter if I extend the MyQuerySchema. How would I get it as a separate parameter of the getmethod, so that I can feed it to the query as a order_byparameter.

kettenbach-it avatar Jan 13 '20 13:01 kettenbach-it

This is what I'd do:

@classmethod
@MyBlp.arguments(MyQuerySchema, location="query")
@MyBlp.response(MySchema(many=True))
@MyBlp.paginate()
def get(cls, args, pagination_parameters):
    sort = args.pop("sort", None)
    query = Mymodel.query.filter_by(**args)
    if sort is not None:
        query = query.order_by(sort)
    pagination_parameters.item_count = query.count()
    return query.paginate(pagination_parameters.page, pagination_parameters.page_size, True).items

This is a bit verbose in the controller. There might be a way to do more in the decorators, depending on the ORM.

lafrech avatar Jan 13 '20 13:01 lafrech

I think you could reduce the pagination boilerplate by defining a custom pagination cursor as shown in the docs.

Or is there a reason you can't do that?

lafrech avatar Jan 13 '20 13:01 lafrech

Concerning the sort parameter: I was really headblocked. It is so easy. Thanks a lot for helping me with that! Maybe a hint in the documentation would be good, since googling for "smorest sort" brings back no usable results and e.g. express.js supports ordering out of the box.

Concerning the custom pagination cursor: If I understand the docs right, it would only save one line of code and it would reduce the complexity of the signature of the paginate call. On the other hand it increases the complexity of the whole thing (one must know about the pagination class etc.), so I'd keep it like it is.

kettenbach-it avatar Jan 13 '20 14:01 kettenbach-it

Up to you, of course.

It would look like this:

@classmethod
@MyBlp.arguments(MyQuerySchema, location="query")
@MyBlp.response(MySchema(many=True))
@MyBlp.paginate(SQLCursorPager)
def get(cls, args, pagination_parameters):
    sort = args.pop("sort", None)
    query = Mymodel.query.filter_by(**args)
    if sort is not None:
        query = query.order_by(sort)
    return query

lafrech avatar Jan 13 '20 14:01 lafrech

If I used more than one field for sorting, e.g.

  • sortorder (asc/desc)
  • sortfield (name of field)

the code will get longer. (How) could this be done in the decorator?

kettenbach-it avatar Jan 13 '20 14:01 kettenbach-it

I have an addendum: currently I cannot sort by nested fields in the above-mentioned way. The reason is that they are not joined in the query. If I do it like this ...

query = Mymodel.query.filter_by(**args)
query = query.outerjoin(Language)
query = query.add_entity(Language)
query = query.order_by("language_title")

... the "language_title" becomes available and I can use it a sort key. The problem is, that this leads to an empty reponse:

[
  {},
  {},
  {},
  {},
  {},
  {},
  {},
  {},
  {},
  {}
]

Probably marshmallow does not how to handle the query-results properly. Can you give a hint, how to sort by fields "behind" a 1:n relation?

kettenbach-it avatar Oct 26 '21 08:10 kettenbach-it

@lafrech Can you give am advice concerning the problem above?

kettenbach-it avatar Oct 28 '21 10:10 kettenbach-it