Skip to content

Latest commit

 

History

History
226 lines (157 loc) · 22.1 KB

data-aggregation-and-recursive-hierarchy-7d91431.md

File metadata and controls

226 lines (157 loc) · 22.1 KB

Data Aggregation and Recursive Hierarchy

The OData V4 Model supports features of the OData Extension for Data Aggregation V4.0 specification.

The $$aggregation binding parameter at sap.ui.model.odata.v4.ODataModel#bindList holds the information needed for data aggregation. It may be changed by sap.ui.model.odata.v4.ODataListBinding#setAggregation. It cannot be combined with an explicit system query option $apply, because it implicitly derives $apply. For more information, see the OData Extension for Data Aggregation V4.0 specification.

Since 1.117.0, either a read-only recursive hierarchy (see below) or (pure) data aggregation is supported, but no mix; hierarchyQualifier is the leading property that decides between these two use cases. Since 1.125.0, maintenance of a recursive hierarchy is supported.

Note:

  • Data aggregation or a recursive hierarchy cannot be combined with grouping via a list binding's first sorter.For more information, see the vGroup parameter of sap.ui.model.Sorter.

  • Data aggregation or a recursive hierarchy do not support the creation, deletion, or refreshing of data. Additional property requests for an entity that already has been requested (see Data Reuse) as well as updating of data including invocation of bound actions and side effects are only supported for a recursive hierarchy.

For every aggregatable property, you can provide the name of the custom aggregate for a corresponding currency or unit of measure. That custom aggregate must return the single value of a unit in case there is only one, or null otherwise ("multi-unit situation"). In the special case that the single value is null, an empty string "" has to be returned.

Normally, there is also a structural property of the same name as the custom aggregate, providing type information, etc. In case of a multi-unit situation, v4.Context#getFilter may be helpful to send a request for more details.

The following client-side instance annotations can be used to access a node level or expansion state. For property bindings, a syntax like {= %{@$ui5.node.level} } is usually helpful, because automatic type determination is not available.

  • @$ui5.node.level – A non-negative integer which describes the node level; "0" is the single root node which corresponds to the grand total row, "1" are the top-level group nodes, etc.

  • @$ui5.node.isExpanded – A boolean which determines whether this node is currently expanded. true means yes, false means no, undefined means that (the state is undefined because) this node is a leaf. As an implementation detail, the annotation might simply be missing for leaves.

  • @$ui5.node.groupLevelCount – An integer value which determines the count of the direct children of a group node. As an implementation detail, the annotation is only available if the corresponding node is expanded.

Two scenarios are supported:

  • You can provide properties for grouping and aggregation. An appropriate system query option $apply is derived from those. The list binding then still provides a flat list of contexts ("rows"), but with additional aggregated properties ("columns"). In addition, you can request grand total values for aggregatable properties. In this case, an extra row appears at the beginning of the flat list of contexts that contains the grand total values, as well as empty values for all other properties.

    Sample Code:

    Example XML View With Grand Total

    <table:Table fixedRowCount="1"
       rows="{
          path : '/BusinessPartners',
          parameters : {
             $$aggregation : {
                aggregate : {
                   SalesAmount : {
                      grandTotal : true,
                      unit : 'Currency'
                   }
                },
                group : {
                   Country : {additionally : ['Texts/Country']}
                }
             },
             $filter : 'SalesAmount gt 1000000',
             $orderby : 'SalesAmount desc'
          }
       }">
       <table:Column template="Texts/Country">
          <Label text="Country"/>
       </table:Column>
       <table:Column hAlign="End" template="SalesAmount">
          <Label text="Sales Amount"/>
       </table:Column>
       <table:Column template="Currency">
          <Label text="Currency"/>
       </table:Column>
    </table:Table>
  • You can provide group levels to determine a hierarchy of expandable group levels in addition to the leaf nodes determined by the groupable and aggregatable properties. To achieve this, specify the names of the group levels in the groupLevels property of $$aggregation. If no other groupable properties are given except those named as levels, the last group level determines the leaf nodes and is not expandable.

    Group levels can be combined with the system query option $count : true; for more information, see Binding Collection Inline Count. Group levels can only be combined with filtering before the aggregation (see below). Note how an $orderby option can address groups across all levels. For every aggregatable property, you can request subtotals and a grand total individually.

    Sample Code:

    Example XML View With Hierarchy

    <table:Table fixedRowCount="1"
       rows="{
          path : '/BusinessPartners',
          parameters : {
             $$aggregation : {
                aggregate : {
                   SalesAmount : {
                      grandTotal : true,
                      subtotals : true,
                      unit : 'Currency'
                   }
                },
                group : {
                    Country : {additionally : ['CountryText']},
                    Region : {additionally : ['RegionText']}
                },
                groupLevels : ['Country','Region','Segment']
             },
             $count : false,
             $orderby : 'Country,Region desc,Segment',
             filters : {path : \'Region\', operator : \'GE\', value1 : \'Mid\'}
          }
       }">
       <table:Column template="CountryText">
          <Label text="Country"/>
       </table:Column>
       <table:Column template="RegionText">
          <Label text="Region"/>
       </table:Column>
       <table:Column template="Segment">
          <Label text="Segment"/>
       </table:Column>
       <table:Column hAlign="End" template="SalesAmount">
          <Label text="Sales Amount"/>
       </table:Column>
       <table:Column template="Currency">
          <Label text="Currency"/>
       </table:Column>
    </table:Table>

For aggregatable properties where grand total or subtotal values are requested, you can globally choose where these should be displayed:

  • at the bottom only,
  • at both the top and bottom,
  • at the top only (default).

Use the grandTotalAtBottomOnly or subtotalsAtBottomOnly property with values true or false, respectively, or simply omit it. For more information, see the API Reference in the Demo Kit.

Filters are provided to the list binding as described in Filtering. The Filter objects are analyzed automatically to perform the filtering before the aggregation where possible using the filter() transformation. The remaining filters, including the provided $filter parameter of the binding, are applied after the aggregation either via the system query option $filter or within the system query option $apply, using again the filter() transformation.

Note that Filter objects are not supported for aggregatable properties with an alias.For more information, see the name property of the aggregate map of the oAggregation parameter of v4.ODataListBinding#setAggregation.

You can provide a search string to be applied before data aggregation via the oAggregation.search parameter of ODataListBinding#setAggregation. It works like the "5.1.7 System Query Option $search", but is applied before data aggregation, not after it. Note that certain content will break the syntax of the $apply system query option when embedded into a search() transformation and thus result in an invalid request. If the OData service supports the ODATA-1452 proposal, then the command system query option when embedded into a ODataUtils.formatLiteral(sSearch, "Edm.String"); should be used to encapsulate the whole search string beforehand (see sap.ui.model.odata.v4.ODataUtils.formatLiteral). Otherwise, it might be wise to restrict your search input accordingly.

For each groupable property, you can define an optional list of strings that provides the paths to properties (like texts or attributes) related to this groupable property in a 1:1 relation. They are requested additionally via groupby and must not change the actual grouping; a unit for an aggregatable property must not be repeated there.

You can display hierarchical data (a "tree") inside a table using a list binding. Read-only hierarchies are supported since 1.117.0 while maintenance is supported since 1.125.0. Such a recursive hierarchy is described by a pair of "Org.OData.Aggregation.V1.RecursiveHierarchy" and "com.sap.vocabularies.Hierarchy.v1.RecursiveHierarchy" annotations at the list binding's entity type. You need to use the same qualifier for both of these annotations, which is known as the hierarchy qualifier.

If the hierarchyQualifier property of $$aggregation is present, a recursive hierarchy without data aggregation is defined. The only other supported properties are expandTo, which optionally specifies the number of initially expanded levels as a positive integer, and search (see Search Before Data Aggregation above). Sorting and filtering can be done as usual (both as system query options and as OpenUI5 objects), but $search is not supported (use search instead, see above).

Note how this influences v4.ODataListBinding#getCount. You can use the v4.ODataListBinding#getAggregation method with the new bVerbose parameter to access some details from the above-mentioned annotations. The v4.ODataListBinding#setAggregation method can be used to change $$aggregation.

The following properties are required from a "com.sap.vocabularies.Hierarchy.v1.RecursiveHierarchy" annotation:

  • DistanceFromRoot
  • DrillState
  • LimitedDescendantCount
  • LimitedRank

Actions and functions can be invoked as usual. Side effects are supported both for single rows and the entire list ("side-effects refresh"; see v4.Context#requestSideEffects for details), even if they affect the hierarchy (node IDs, parent/child relations, or sibling order) itself. The current tree state with respect to expanded and collapsed nodes (see v4.Context#isExpanded) is kept even in case of such a side-effects refresh.

The @$ui5.node.level and @$ui5.node.isExpanded client-side instance annotations can be used as described above to access a node level or expansion state. A context's index refers to its position in the list binding's "flat" collection. You can use v4.Context#getParent to access a node's parent. If the parent is not yet known, v4.Context#requestParent can be used to request it from the server. The v4.Context#isAncestorOf API also helps to inspect the parent/child relationship (note that v4.ODataListBinding#getRootBinding is unrelated).

Since 1.125.0, a recursive hierarchy need not be read-only, but maintenance is supported, namely:

  • Update of arbitrary properties, including any corresponding side effects. Note that a side-effects refresh needs to be requested explicitly if the change affects the hierarchy (node IDs, parent/child relations, or sibling order). This is not done by the model itself.
  • Creation of new nodes, either as new root nodes or below an existing parent node. Creation is even supported if the parent was a leaf before, however it is not supported for a collapsed parent.For more details, see v4.ODataListBinding#create and "@$ui5.node.parent" therein. Since 1.130.0, the createInPlace option is supported for the$$aggregation binding parameter and the v4.ODataListBinding#setAggregation method. When set, newly created nodes are shown "in place", i.e. at the position specified by the service . Otherwise, created nodes are displayed out of place as the first children of their parent or as the first roots, but not in their usual position as defined by the service and the current sort order.
  • Deletion of existing nodes, see v4.Context#delete. Note that the deletion is first done on the server and only later shown on the client. Thus, the group ID must not have submit mode "API".
  • Moving of nodes. You can change the parent node, including turning a child node into a root node and vice versa, and you can also change the sibling position, including making a node the last one among its siblings or moving it just before a specified sibling. For more details, see v4.Context#move. Note that nextSibling requires a "com.sap.vocabularies.Hierarchy.v1.RecursiveHierarchyActions" annotation with at least a ChangeNextSiblingAction. An out-of-place node has no preceding sibling and thus cannot be moved up. Each out-of-place node with the same parent has the same following sibling, namely that parent's first in-place (that is, not out-of-place) node. That following sibling is null if the parent has no in-place children. A similar consideration applies for out-of-place root nodes. This way, you can move out-of-place nodes down so that they become in-place nodes. Thus, you actively determine their position among their siblings. A first in-place node has no preceding sibling even if out-of-place nodes are present. Thus, an in-place node cannot be moved up in order to become out-of-place (again).

Note that only one such change must be pending at any point in time. That is, you must wait for one change to be completed before starting the next change. The only exception is property updates, for which multiple properties can be combined as usual.

Example Requests

The TopLevels function is fundamental for recursive hierarchies. It describes the input set underlying the hierarchy (see the list binding's path) and specifies which recursive hierarchy is built on top (see hierarchyQualifier above). It takes care to initially expand a certain number of levels (see expandTo above) and later to expand or collapse certain nodes in order to keep the tree state during a side-effects refresh.

A typical request to read the first page of a hierarchical table may look like this:

GET /sap/opu/odata4/IWBEP/TEA/default/IWBEP/TEA_BUSI/0001/EMPLOYEES?$apply=orderby(AGE)/com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/EMPLOYEES,HierarchyQualifier='OrgChart',NodeProperty='ID',Levels=2)&$select=AGE,DescendantCount,DistanceFromRoot,DrillState,ID,MANAGER_ID,Name&$count=true&$skip=0&$top=115

Note how the sibling(!) order is specified via an orderby transformation (see the list binding's sorters as well as $orderby). The list binding's path would be "/EMPLOYEES", and $$aggregation looks as follows. The model's autoExpandSelect parameter does its magic, and $count&$skip&$top is taken care of automatically by the list binding.

<Table rows="{
    path: '/EMPLOYEES',
    parameters: {
        $count: false,
        $orderby: 'AGE',
        $$aggregation: {
            expandTo: 2,
            hierarchyQualifier: 'OrgChart'
        },
        $$patchWithoutSideEffects: true
    },
    ...
}">

If the list binding uses $count: true, for example, to show this count as part of a title, an extra request is sent once (not each time when scrolling, but of course again after a (side-effects) refresh). It includes any custom query options as well as filter and search criteria: GET /sap/opu/odata4/IWBEP/TEA/default/IWBEP/TEA_BUSI/0001/EMPLOYEES/$count?sap-client=123&$filter=AGE ge 0 and (Is_Manager)&$search=developer

With filter and search, the main request looks a bit more complicated and includes an ancestors transformation beforehand: GET /sap/opu/odata4/IWBEP/TEA/default/IWBEP/TEA_BUSI/0001/EMPLOYEES?$apply=ancestors($root/EMPLOYEES,OrgChart,ID,filter(AGE ge 0 and (Is_Manager))/search(developer),keep start)/orderby(AGE)/com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/EMPLOYEES,HierarchyQualifier='OrgChart',NodeProperty='ID',Levels=2) &$select=AGE,DescendantCount,DistanceFromRoot,DrillState,ID,MANAGER_ID,Name&$count=true&$skip=0&$top=115

When a node is expanded individually, a request for its children is sent using a descendants transformation, for example: GET /sap/opu/odata4/IWBEP/TEA/default/IWBEP/TEA_BUSI/0001/EMPLOYEES?$apply=descendants($root/EMPLOYEES,OrgChart,ID,filter(ID eq '0'),1)/orderby(AGE)&$select=AGE,DrillState,ID,MANAGER_ID,Name&$count=true&$skip=0&$top=6

When keeping the expand/collapsed state of nodes, the TopLevels function's ExpandLevels parameter is needed, for example: GET /sap/opu/odata4/IWBEP/TEA/default/IWBEP/TEA_BUSI/0001/EMPLOYEES?$apply=com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/EMPLOYEES,HierarchyQualifier='OrgChart',NodeProperty='ID',Levels=2,ExpandLevels=[{NodeID : "8", Levels : 0}, {NodeID : "1", Levels : 1}])&...

When moving a node, it is PATCHed with a payload like "[email protected]" : "EMPLOYEES('9')", which points to the new parent. When creating a new node, a similar payload is used to point to the new parent as part of the POST. For root nodes, a null value is sent. To determine a node's sibling position, the "com.sap.vocabularies.Hierarchy.v1.RecursiveHierarchyActions" annotation's ChangeNextSiblingAction is invoked with a payload like NextSibling : {ID : "3" }. A null value is used to make a node the last sibling.