doc/book/devrepo/entityclasses/application-logic.rst
changeset 10491 c67bcee93248
parent 9316 38518284c200
equal deleted inserted replaced
10490:76ab3c71aff2 10491:c67bcee93248
       
     1 How to use entities objects and adapters
       
     2 ----------------------------------------
       
     3 
       
     4 The previous chapters detailed the classes and methods available to
       
     5 the developer at the so-called `ORM`_ level. However they say little
       
     6 about the common patterns of usage of these objects.
       
     7 
       
     8 .. _`ORM`: http://en.wikipedia.org/wiki/Object-relational_mapping
       
     9 
       
    10 Entities objects (and their adapters) are used in the repository and
       
    11 web sides of CubicWeb. On the repository side of things, one should
       
    12 manipulate them in Hooks and Operations.
       
    13 
       
    14 Hooks and Operations provide support for the implementation of rules
       
    15 such as computed attributes, coherency invariants, etc (they play the
       
    16 same role as database triggers, but in a way that is independent of
       
    17 the actual data sources).
       
    18 
       
    19 So a lot of an application's business rules will be written in Hooks
       
    20 (or Operations).
       
    21 
       
    22 On the web side, views also typically operate using entity
       
    23 objects. Obvious entity methods for use in views are the Dublin Core
       
    24 methods like ``dc_title``. For separation of concerns reasons, one
       
    25 should ensure no ui logic pervades the entities level, and also no
       
    26 business logic should creep into the views.
       
    27 
       
    28 In the duration of a transaction, entities objects can be instantiated
       
    29 many times, in views and hooks, even for the same database entity. For
       
    30 instance, in a classic CubicWeb deployment setup, the repository and
       
    31 the web front-end are separated process communicating over the
       
    32 wire. There is no way state can be shared between these processes
       
    33 (there is a specific API for that). Hence, it is not possible to use
       
    34 entity objects as messengers between these components of an
       
    35 application. It means that an attribute set as in ``obj.x = 42``,
       
    36 whether or not x is actually an entity schema attribute, has a short
       
    37 life span, limited to the hook, operation or view within which the
       
    38 object was built.
       
    39 
       
    40 Setting an attribute or relation value can be done in the context of a
       
    41 Hook/Operation, using the ``obj.cw_set(x=42)`` notation or a plain
       
    42 RQL ``SET`` expression.
       
    43 
       
    44 In views, it would be preferable to encapsulate the necessary logic in
       
    45 a method of an adapter for the concerned entity class(es). But of
       
    46 course, this advice is also reasonable for Hooks/Operations, though
       
    47 the separation of concerns here is less stringent than in the case of
       
    48 views.
       
    49 
       
    50 This leads to the practical role of objects adapters: it's where an
       
    51 important part of the application logic lies (the other part being
       
    52 located in the Hook/Operations).
       
    53 
       
    54 Anatomy of an entity class
       
    55 --------------------------
       
    56 
       
    57 We can look now at a real life example coming from the `tracker`_
       
    58 cube. Let us begin to study the ``entities/project.py`` content.
       
    59 
       
    60 .. sourcecode:: python
       
    61 
       
    62     from cubicweb.entities.adapters import ITreeAdapter
       
    63 
       
    64     class ProjectAdapter(ITreeAdapter):
       
    65         __select__ = is_instance('Project')
       
    66         tree_relation = 'subproject_of'
       
    67 
       
    68     class Project(AnyEntity):
       
    69         __regid__ = 'Project'
       
    70         fetch_attrs, cw_fetch_order = fetch_config(('name', 'description',
       
    71                                                     'description_format', 'summary'))
       
    72 
       
    73         TICKET_DEFAULT_STATE_RESTR = 'S name IN ("created","identified","released","scheduled")'
       
    74 
       
    75         def dc_title(self):
       
    76             return self.name
       
    77 
       
    78 The fact that the `Project` entity type implements an ``ITree``
       
    79 interface is materialized by the ``ProjectAdapter`` class (inheriting
       
    80 the pre-defined ``ITreeAdapter`` whose ``__regid__`` is of course
       
    81 ``ITree``), which will be selected on `Project` entity types because
       
    82 of its selector. On this adapter, we redefine the ``tree_relation``
       
    83 attribute of the ``ITreeAdapter`` class.
       
    84 
       
    85 This is typically used in views concerned with the representation of
       
    86 tree-like structures (CubicWeb provides several such views).
       
    87 
       
    88 It is important that the views themselves try not to implement this
       
    89 logic, not only because such views would be hardly applyable to other
       
    90 tree-like relations, but also because it is perfectly fine and useful
       
    91 to use such an interface in Hooks.
       
    92 
       
    93 In fact, Tree nature is a property of the data model that cannot be
       
    94 fully and portably expressed at the level of database entities (think
       
    95 about the transitive closure of the child relation). This is a further
       
    96 argument to implement it at entity class level.
       
    97 
       
    98 ``fetch_attrs`` configures which attributes should be pre-fetched when using ORM
       
    99 methods retrieving entity of this type. In a same manner, the ``cw_fetch_order`` is
       
   100 a class method allowing to control sort order. More on this in :ref:`FetchAttrs`.
       
   101 
       
   102 We can observe the big ``TICKET_DEFAULT_STATE_RESTR`` is a pure
       
   103 application domain piece of data. There is, of course, no limitation
       
   104 to the amount of class attributes of this kind.
       
   105 
       
   106 The ``dc_title`` method provides a (unicode string) value likely to be
       
   107 consumed by views, but note that here we do not care about output
       
   108 encodings. We care about providing data in the most universal format
       
   109 possible, because the data could be used by a web view (which would be
       
   110 responsible of ensuring XHTML compliance), or a console or file
       
   111 oriented output (which would have the necessary context about the
       
   112 needed byte stream encoding).
       
   113 
       
   114 .. note::
       
   115 
       
   116   The Dublin Core `dc_xxx` methods are not moved to an adapter as they
       
   117   are extremely prevalent in CubicWeb and assorted cubes and should be
       
   118   available for all entity types.
       
   119 
       
   120 Let us now dig into more substantial pieces of code, continuing the
       
   121 Project class.
       
   122 
       
   123 .. sourcecode:: python
       
   124 
       
   125     def latest_version(self, states=('published',), reverse=None):
       
   126         """returns the latest version(s) for the project in one of the given
       
   127         states.
       
   128 
       
   129         when no states specified, returns the latest published version.
       
   130         """
       
   131         order = 'DESC'
       
   132         if reverse is not None:
       
   133             warn('reverse argument is deprecated',
       
   134                  DeprecationWarning, stacklevel=1)
       
   135             if reverse:
       
   136                 order = 'ASC'
       
   137         rset = self.versions_in_state(states, order, True)
       
   138         if rset:
       
   139             return rset.get_entity(0, 0)
       
   140         return None
       
   141 
       
   142     def versions_in_state(self, states, order='ASC', limit=False):
       
   143         """returns version(s) for the project in one of the given states, sorted
       
   144         by version number.
       
   145 
       
   146         If limit is true, limit result to one version.
       
   147         If reverse, versions are returned from the smallest to the greatest.
       
   148         """
       
   149         if limit:
       
   150             order += ' LIMIT 1'
       
   151         rql = 'Any V,N ORDERBY version_sort_value(N) %s ' \
       
   152               'WHERE V num N, V in_state S, S name IN (%s), ' \
       
   153               'V version_of P, P eid %%(p)s' % (order, ','.join(repr(s) for s in states))
       
   154         return self._cw.execute(rql, {'p': self.eid})
       
   155 
       
   156 .. _`tracker`: http://www.cubicweb.org/project/cubicweb-tracker/
       
   157 
       
   158 These few lines exhibit the important properties we want to outline:
       
   159 
       
   160 * entity code is concerned with the application domain
       
   161 
       
   162 * it is NOT concerned with database consistency (this is the realm of
       
   163   Hooks/Operations); in other words, it assumes a consistent world
       
   164 
       
   165 * it is NOT (directly) concerned with end-user interfaces
       
   166 
       
   167 * however it can be used in both contexts
       
   168 
       
   169 * it does not create or manipulate the internal object's state
       
   170 
       
   171 * it plays freely with RQL expression as needed
       
   172 
       
   173 * it is not concerned with internationalization
       
   174 
       
   175 * it does not raise exceptions
       
   176 
       
   177