From 1bc81bc604e1eaf9497783ae61da6eccdbee0ec5 Mon Sep 17 00:00:00 2001 From: Subhash Bhushan Date: Fri, 5 Apr 2019 18:29:42 -0700 Subject: [PATCH] Entity Queries Documentation --- docs/api/entity.rst | 18 +++++ docs/api/queryset.rst | 43 +++++++++++ docs/user/entities/caching.rst | 5 ++ docs/user/entities/field-lookups.rst | 4 ++ docs/user/entities/index.rst | 8 ++- docs/user/entities/lifecycle.rst | 49 ++++++++++--- docs/user/entities/persistence.rst | 2 - docs/user/entities/q-objects.rst | 4 ++ docs/user/entities/querying.rst | 103 ++++++++++++++++++++++++++- 9 files changed, 217 insertions(+), 19 deletions(-) create mode 100644 docs/user/entities/caching.rst create mode 100644 docs/user/entities/field-lookups.rst delete mode 100644 docs/user/entities/persistence.rst create mode 100644 docs/user/entities/q-objects.rst diff --git a/docs/api/entity.rst b/docs/api/entity.rst index f1b59359..dfb19154 100644 --- a/docs/api/entity.rst +++ b/docs/api/entity.rst @@ -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 + diff --git a/docs/api/queryset.rst b/docs/api/queryset.rst index fa6346aa..e253477a 100644 --- a/docs/api/queryset.rst +++ b/docs/api/queryset.rst @@ -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 + diff --git a/docs/user/entities/caching.rst b/docs/user/entities/caching.rst new file mode 100644 index 00000000..3ba41154 --- /dev/null +++ b/docs/user/entities/caching.rst @@ -0,0 +1,5 @@ +.. _entity-queryset-caching: + +Caching +~~~~~~~ + diff --git a/docs/user/entities/field-lookups.rst b/docs/user/entities/field-lookups.rst new file mode 100644 index 00000000..e7b7afd3 --- /dev/null +++ b/docs/user/entities/field-lookups.rst @@ -0,0 +1,4 @@ +.. _entity-queryset-field-lookups: + +Field Lookups +------------- \ No newline at end of file diff --git a/docs/user/entities/index.rst b/docs/user/entities/index.rst index 4f0656da..768fa60e 100644 --- a/docs/user/entities/index.rst +++ b/docs/user/entities/index.rst @@ -49,11 +49,13 @@ Entity lifecycle lifecycle -Persistence and Querying ------------------------- +Querying +-------- .. toctree:: :maxdepth: 1 - persistence querying + field-lookups + q-objects + caching \ No newline at end of file diff --git a/docs/user/entities/lifecycle.rst b/docs/user/entities/lifecycle.rst index 5e8ddbca..251b6867 100644 --- a/docs/user/entities/lifecycle.rst +++ b/docs/user/entities/lifecycle.rst @@ -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. @@ -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) @@ -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. @@ -44,8 +47,8 @@ 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: @@ -53,10 +56,10 @@ To retrieve an entity by its primary key, you can use the :ref:`api-entity-get` >>> 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: @@ -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: @@ -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() diff --git a/docs/user/entities/persistence.rst b/docs/user/entities/persistence.rst deleted file mode 100644 index 871819a1..00000000 --- a/docs/user/entities/persistence.rst +++ /dev/null @@ -1,2 +0,0 @@ -Persisting Entities -------------------- \ No newline at end of file diff --git a/docs/user/entities/q-objects.rst b/docs/user/entities/q-objects.rst new file mode 100644 index 00000000..f24ecc14 --- /dev/null +++ b/docs/user/entities/q-objects.rst @@ -0,0 +1,4 @@ +.. _entity-queryset-q-objects: + +Complex lookups with Q objects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/user/entities/querying.rst b/docs/user/entities/querying.rst index 09257b90..d933e0ae 100644 --- a/docs/user/entities/querying.rst +++ b/docs/user/entities/querying.rst @@ -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 + + Retrieving objects ~~~~~~~~~~~~~~~~~~ @@ -15,7 +24,7 @@ You get a QuerySet by using your Entity class, like so: .. code-block:: python >>> Customer.query - + >>> c = Customer(firstname='John', lastname='Doe') >>> c.query Traceback: @@ -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 +~~~~~~~~~~~ + +**