diff --git a/src/doc/tutorials/index.rst b/src/doc/tutorials/index.rst index 706e26c2b1b4876c29d43c2bddd9a5fe357a003d..e745482ace189e10a042975869cae6310f6ad703 100644 --- a/src/doc/tutorials/index.rst +++ b/src/doc/tutorials/index.rst @@ -15,6 +15,7 @@ advanced usage of the Python client. Data-Insertion errors Entity-Getters + paginated_queries caching data-model-interface complex_data_models diff --git a/src/doc/tutorials/paginated_queries.rst b/src/doc/tutorials/paginated_queries.rst new file mode 100644 index 0000000000000000000000000000000000000000..c250223f46405caa9d289ce4d8774daf06fdf366 --- /dev/null +++ b/src/doc/tutorials/paginated_queries.rst @@ -0,0 +1,63 @@ +Query pagination +================ + +When retrieving many entities, you may not want to retrieve all at once, e.g., +for performance reasons or to prevent connection timeouts, but rather in a +chunked way. For that purpose, there is the ``page_length`` parameter in the +:py:meth:`~linkahead.common.models.execute_query` function. If this is set to a +non-zero integer, the behavior of the function changes in that it returns a +Python `generator <https://docs.python.org/3/glossary.html#term-generator>`_ +which can be used, e.g., in loops or in list comprehension. The generator yields +a :py:class:`~linkahead.common.models.Container` containing the next +``page_length`` many entities from the query result. + +The following example illustrates this on the demo server. + +.. code-block:: python + + import linkahead as db + + # 10 at the time of writing of this example + print(db.execute_query("FIND MusicalInstrument")) + + # Retrieve in pages of length 5 and iterate over the pages + for page in db.execute_query("FIND MusicalInstrument", page_length=5): + # each page is a container + print(type(page)) + # exactly page_length=5 for the first N-1 pages, + # and possibly less for the last page + print(len(page)) + # the items on each page are subclasses of Entity + print(type(page[0])) + # The id of the first entity on the page is different for all pages + print(page[0].id) + + # You can use this in a list comprehension to fill a container + container_paginated = db.Container().extend( + [ent for page in db.execute_query("FIND MusicalInstrument", page_length=5) for ent in page] + ) + # The result is the same as in the unpaginated case, but the + # following can cause connection timeouts in case of very large + # retrievals + container_at_once = db.execute_query("FIND MusicalInstrument") + for ent1, ent2 in zip(container_paginated, container_at_once): + print(ent1.id == ent2.id) # always true + +As you can see, you can iterate over a paginated query and then access the +entities on each page during the iteration. + +.. note:: + + The ``page_length`` keyword is ignored for ``COUNT`` queries where + :py:meth:`~linkahead.common.models.execute_query` always returns the integer + result and in case of ``unique=True`` where always exactly one + :py:class:`~linkahead.common.models.Entity` is returned. + + +.. warning:: + + Be careful when combining query pagination with insert, update, or delete + operations. If your database changes while iterating over a paginated query, + the client will raise a + :py:exc:`~linkahead.exceptions.PagingConsistencyError` since the server + can't guarantee that the query results haven't changed in the meantime. diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py index ea537ffe8c44a7a7fb79c2d4080b63f9b3da2284..8565c2e7061aeb1dd6e02a34ad4ca867327a6e95 100644 --- a/src/linkahead/common/models.py +++ b/src/linkahead/common/models.py @@ -4526,8 +4526,8 @@ def execute_query(q, unique=False, raise_exception_on_error=True, cache=True, Otherwise, paging is disabled, as well as for count queries and when unique is True. Defaults to None. - Raises: - ------- + Raises + ------ PagingConsistencyError If the database state changed between paged requests.