Skip to content

Commit

Permalink
Entity Queries Documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
subhashb committed Apr 6, 2019
1 parent 90b0a43 commit 1bc81bc
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 19 deletions.
18 changes: 18 additions & 0 deletions docs/api/entity.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,21 @@ Lifecycle Methods:
^^^^^^^^^^^^

.. automethod:: protean.core.entity.Entity.delete

Other Methods:
~~~~~~~~~~~~~~

.. _api-entity-exists:

``exists()``
^^^^^^^^^^^^

.. automethod:: protean.core.entity.Entity.exists

.. _api-entity-find-by:

``find_by()``
^^^^^^^^^^^^^

.. automethod:: protean.core.entity.Entity.find_by

43 changes: 43 additions & 0 deletions docs/api/queryset.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,46 @@ QuerySet
^^^^^^^^^

.. automethod:: protean.core.entity.QuerySet.all

.. _api-queryset-filter:

``filter``
^^^^^^^^^^

.. automethod:: protean.core.entity.QuerySet.filter

.. _api-queryset-exclude:

``exclude``
^^^^^^^^^^^

.. automethod:: protean.core.entity.QuerySet.exclude

.. _api-queryset-update:

``update``
^^^^^^^^^^

.. automethod:: protean.core.entity.QuerySet.update

.. _api-queryset-delete:

``delete``
^^^^^^^^^^^^^^^^

.. automethod:: protean.core.entity.QuerySet.delete

.. _api-queryset-update-all:

``update_all``
^^^^^^^^^^^^^^

.. automethod:: protean.core.entity.QuerySet.update_all

.. _api-queryset-delete-all:

``delete_all``
^^^^^^^^^^^^^^

.. automethod:: protean.core.entity.QuerySet.delete_all

5 changes: 5 additions & 0 deletions docs/user/entities/caching.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.. _entity-queryset-caching:

Caching
~~~~~~~

4 changes: 4 additions & 0 deletions docs/user/entities/field-lookups.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.. _entity-queryset-field-lookups:

Field Lookups
-------------
8 changes: 5 additions & 3 deletions docs/user/entities/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@ Entity lifecycle

lifecycle

Persistence and Querying
------------------------
Querying
--------

.. toctree::
:maxdepth: 1

persistence
querying
field-lookups
q-objects
caching
49 changes: 38 additions & 11 deletions docs/user/entities/lifecycle.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
Entity LifeCycle
----------------
.. _entity-lifecycle:

CRUD
----

Once the entities are defined, you can use a database-agnostic API to create, query, update, and delete objects. Let us explore the different options to manage the lifecycle of an Entity.

Expand All @@ -15,6 +17,7 @@ Throughout this guide, we will refer to the following models as example:
lastname = field.String(required=True, max_length=50)
date_of_birth = field.Date()
ssn = field.String(max_length=50)
countrycode = field.String(max_length=3)
class Airline(Entity):
name = field.String(required=True, max_length=50)
Expand All @@ -25,8 +28,8 @@ Throughout this guide, we will refer to the following models as example:
departure = field.DateTime()
arrival = field.DateTime()
Creating objects
~~~~~~~~~~~~~~~~
Creating Entities
~~~~~~~~~~~~~~~~~

An Entity typically maps to a database schema, and an instance of the entity represents a particular item in the database. The item could be a row in a table if an RDBMS such as MySQL and Postgresql is being used, or a document if its a document-oriented like MongoDB, or a key value pair in a data structure store like Redis.

Expand All @@ -44,19 +47,19 @@ Assuming your entities have been defined in app/flight/entities.py, here's an ex

Once saved, an entity instance will be associated with a unique primary identifier that can be used to retrieve it later.

Retrieving objects
~~~~~~~~~~~~~~~~~~
Retrieving Entities
~~~~~~~~~~~~~~~~~~~

To retrieve an entity by its primary key, you can use the :ref:`api-entity-get` method on the Entity class. Assuming that customer object above was saved with an identifier say 1, you can retrieve the customer object like so:

.. code-block:: python
>>> old_customer = Customer.get(1)
It is also possible to specify filter criteria to retreive specific sections of items. Refer to :ref:`queryset` documentation for detailed information on constructing such criteria and fetching items.
It is also possible to specify filter criteria to retreive specific sections of items. Refer to :ref:`entity-queryset` documentation for detailed information on constructing such criteria and fetching items.

Updating objects
~~~~~~~~~~~~~~~~
Updating Entities
~~~~~~~~~~~~~~~~~

If you want to update an object that's already in the database, it's as simple as changing its attributes and calling :ref:`api-entity-save` on the object:

Expand All @@ -73,8 +76,20 @@ You can also do this operation in one step, by supplying the details to be updat
:ref:`api-entity-update` can accept either keyword arguments containing attribute-value pairs, or a dictionary of key-values.

Deleting objects
~~~~~~~~~~~~~~~~
If you want to mass update entities matching a set of criteria, you can call :ref:`api-queryset-update` on a queryset:

.. code-block:: python
>>> Customer.filter(firstname='John').update(firstname='Jane')
If you wanted to do the same update but without running validations, you can use :ref:`api-queryset-update-all`:

.. code-block:: python
>>> Customer.filter(firstname='John').update_all(firstname='Jane')
Deleting Entities
~~~~~~~~~~~~~~~~~

To remove items from the database, you can simply call :ref:`api-entity-delete` on the entity instance:

Expand All @@ -83,3 +98,15 @@ To remove items from the database, you can simply call :ref:`api-entity-delete`
>>> old_customer.delete()
A call to :ref:`api-entity-delete` returns the deleted entity.

You can also delete entities matching a specific criteria by calling :ref:`api-queryset-delete`:

.. code-block:: python
>>> Customer.filter(firstname='John').delete()
Note that :ref:`api-queryset-delete` above loops through each entity and calls the ``delete()`` method in each object in order to trigger all validations and callbacks. If you wanted to delete entities without running validations or cascades, you can use :ref:`api-queryset-delete-all`:

.. code-block:: python
>>> Customer.filter(firstname='John').delete_all()
2 changes: 0 additions & 2 deletions docs/user/entities/persistence.rst

This file was deleted.

4 changes: 4 additions & 0 deletions docs/user/entities/q-objects.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.. _entity-queryset-q-objects:

Complex lookups with Q objects
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
103 changes: 100 additions & 3 deletions docs/user/entities/querying.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
.. _queryset:
.. _entity-queryset:

Querying
--------

To retrieve items from your database, construct a QuerySet through the ``query`` object on your Entity class. A QuerySet represents a collection of objects from your database. It can have zero, one or many filters. Filters narrow down the query results based on the given criteria.

You can access the queryset object directly via the Entity class, like so:

.. code-block:: python
>>> Customer.query
<QuerySet: entity: Customer, criteria: ('protean.utils.query.Q', (), {}), ...>
Retrieving objects
~~~~~~~~~~~~~~~~~~

Expand All @@ -15,7 +24,7 @@ You get a QuerySet by using your Entity class, like so:
.. code-block:: python
>>> Customer.query
<QuerySet: entity: Customer, criteria: ...>
<QuerySet: entity: Customer, criteria: ...>
>>> c = Customer(firstname='John', lastname='Doe')
>>> c.query
Traceback:
Expand All @@ -27,10 +36,98 @@ In the above example, you get an error if you try to access a QuerySet on an ins
Retrieving all objects
~~~~~~~~~~~~~~~~~~~~~~

The simplest way to retrieve all items of an Entity is to get all of them. To do this, use the all() method on a QuerySet:
The most straightforward way to retrieve objects of a particular Entity type from a database is to get all of them. You can use the :ref:`api-queryset-all` method on a QuerySet:

.. code-block:: python
>>> all_customers = Customer.query.all()
The :ref:`api-queryset-all` method returns all items in the database.

Retrieving specific objects with filters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

To retrieve a subset of objects in the database, you would apply filter criteria on a QuerySet. The two most common ways to define the criteria are:

``filter(*args, **kwargs)``
^^^^^^^^^^^^^^^^^^^^^^^^^^^

Returns a new :ref:`api-queryset` object containing items that match the given criteria.

.. code-block:: python
>>> Customer.query.filter(lastname='Doe')
``exclude(*args, **kwargs)``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Returns a new :ref:`api-queryset` object containing items that do **NOT** match the given criteria.

.. code-block:: python
>>> Customer.query.exclude(firstname='John')
The criteria supplied to these methods should be in the format described in :ref:`entity-queryset-field-lookups`.

QuerySet Properties
~~~~~~~~~~~~~~~~~~~

Chainability
^^^^^^^^^^^^

These methods return a new :ref:`api-queryset`, so it is possible to chain criteria together. For example:

.. code-block:: python
>>> Customer.query.filter(lastname='Doe').exclude(firstname='John')
Immutability
^^^^^^^^^^^^

Each time you refine a **QuerySet**, you get a brand-new QuerySet that is independent of the previous **QuerySet** but carries forward the filter criteria built so far. Each refinement creates a separate and distinct QuerySet that can be stored, used and reused.

.. code-block:: python
>>> query1 = Customer.query.filter(firstname='John')
>>> query2 = query1.filter(date_of_birth__gte=datetime.date.today() - relativedelta(years=35))
>>> query3 = query2.exclude(countrycode='US')
>>> assert query1 != query2 != query3
>>> young_johns_outside_us = query3.all()
Lazy Evaluation
^^^^^^^^^^^^^^^

Querysets are not evaluated on creation. You can refine criteria in multiple passes, stacking up filters in the final queryset object, before calling for an evaluation and fetching results.

You can evaluate a **QuerySet** in the following ways:

* Iteration: A **QuerySet** is iterable, and it executes its database query the first time you iterate over it.

.. code-block:: python
for customer in Customer.query.all():
print(customer.firstname)
* Slicing: A **QuerySet** can be sliced, using Python's array-slicing syntax. Slicing an unevaluated **QuerySet** usually returns another unevaluated QuerySet, but the database query will be executed if you use the "step" parameter of slice syntax, and will return a list. Slicing a QuerySet that has been evaluated also returns a list.

* **len()**: A **QuerySet** is evaluated when you call len() on it, returning the length of the result set.

* list(): Explicitly calling **list()** on a **QuerySet** object forces its evaluation:

.. code-block:: python
johns = list(Customer.query.filter(firstname='John'))
* bool(): Testing a **QuerySet** in a boolean context, such as using **bool()**, **or**, **and** or an **if** statement will cause it to be executed. If there is at least one result, the **QuerySet** is **True**, otherwise **False**.

.. code-block:: python
if Customer.query.filter(firstname='John'):
print("Customers with Firstname `John` found")
You get the same effect if you were calling :ref:`api-entity-exists` on the Entity with filter criteria.

Raw Queries
~~~~~~~~~~~

*<TO BE DOCUMENTED>*

0 comments on commit 1bc81bc

Please sign in to comment.