With this post I want to continue the blog series “A Programming Model for Business Applications”. If you havn´t read the first part in the series, I recommend that you read the first part go back to A Programming Model for Business Applications (1): Assumptions, Building Blocks, and Example App and read it first.
In this part, I will discuss the implementation of the entity model introduced in Figure 2 of part 1 with HANA Core Data Services (HANA CDS).
Note: In HANA SPS8 the implementation suffers from a couple of limitations in HANA CDS, in particular:
- Missing support for references between CDS files
- Missing support for “unmanaged associations” (= ability to define an association on existing elements using “on”)
- Missing support for “Boolean” data type
- Missing support for calculated elements in entities
According to my knowledge, these limitations will be removed in SPS9, so it is worth checking the SPS9 documentation as soon as it is available!
I implement the model in one CDS file (due to a restriction in HANA SPS8) in four sub-contexts. So, here is the stub of my CDS file:
namespace XYZ;
@Schema:'XYZ'
context bo {
context common {
(...)
};
context address {
(...)
};// end of context address
context businesspartner {
(...)
};// end of context businesspartner
context salesterritorymgmt {
(...)
};// end of context salesterritorymgmt
context salesorder {
(...)
};// end of context salesorder
}; // end of context bo
Context Common
In the common context, I implement common data types.
Code lists are implemented as entities, for example:
entity Currency {
key Code :String(5000);
DescriptionText :String(5000);
};
Code contains the code representation, for example “EN” for English, DescriptionText contains the description in default language.
A code list table may not only contain the code and the default description, but also additional elements (attributes), for example:
entity IncotermsClassification {
key Code :String(5000);
DescriptionText :String(5000);
LocationIsMandatoryIndicator :bo.common.Indicator;// workaround for "Boolean"
// IncotermsClassificationCode - Examples
// CFR Cost and freight
// EXW Ex works
// FCA Free carrier
};
As CDS does not support yet the data type “Boolean” as a native data type, I have defined an Indicator data type; I use this data type whenever a Boolean is required:
type Indicator :Integer;// WORKAROUND: Boolean is not supported
Structured re-use data types are implemented as follows:
type Amount {
Currency :association[0..1]to common.Currency;
Value :DecimalFloat;
};
type Incoterms {
Classification :associationto bo.common.IncotermsClassification;
TransferLocationName :String(5000);
};
Sales Order
The header (or main, root) entity of the Sales Order business object is implemented as follows:
entity SimpleOrder {
key ID_ :Integer64;
SystemAdministrativeData :bo.common.SystemAdministrativeData;
ID :String(35);
Name :String(256);
//@EndUserText : { label: 'Posting Date'}
DateTime :UTCDateTime;
Currency :association[0..1]to bo.common.Currency;
FulfilmentBlockingReason :association[0..1]to bo.common.FulfilmentBlockingReason;
DeliveryPriorityCode :association[0..1]to bo.common.Priority;
Incoterms :bo.common.Incoterms;
RequestedFulfilmentDateTime :UTCDateTime;
PaymentFormCode :association[0..1]to bo.common.PaymentForm;
Status :bo.simpleorder.Status;
};
Discussion:
1) For each BO entity, we introduce a technical key of type big integer (Integer64 in CDS), which is named ID_ (which dangling “_”) and which should not be mixed up with the semantic key, which is in this case represented by the element “ID”. Exposing a unified simple key makes the work much easier in the
business logic and the UI implementation (instead of dealing with the “semantic” key, which may differ from entity to entity (for example many entities have semantic keys that consist of multiple elements).
2) Code lists are represented by associations to code list entities. Please note that the resulting database field has the name FulfilmentBlockingReason.Code.
3) CDS does not allow (in SPS8) defining inline structured elements (so-called Anonymous structure types) within an entity, for example:
entity SimpleOrder {
(...)
Status {
LifeCycleStatus :association[0..1]to salesorder.LifeCycleStatus;
InvoiceProcessingStatus :association[0..1]to ref1.salesorder.InvoiceProcessingStatus;
};
As a workaround, I had to define all structured data types using a type statement and refer it in the entity.
4) CDS does not allow (in SPS8) defining calculated fields directly in the entity (limitation for HANA SPS8). We will come to this point later in part 3 of this series.
The Item entity is modelled as follows:
entity Item {
key ID_ :Integer64;
Parent_ID_ :Integer64;
SystemAdministrativeData :bo.common.SystemAdministrativeData;
Product :association[0..1]to bo.product.Product;
Quantity :common.Quantity;
RequestedFulfilmentDateTime :UTCDateTime;
ListPriceAmount :bo.common.Amount;
DiscountPercentValue :Decimal(5,2);
};
Discussion
The relation to the Sales Order header entity is stored in the element Parent_ID_ (I use here again a dangling “_” to mark this element as “technical”). CDS does not know the construct of a “composition association”, which would be the optimal way of defining the relation between header and item. So I decided to
introduce the Parent_ID_ element. The associations between header and item could be modelled as so-called unmanaged associations in the following way:
In the Sales Order entity:
Item :association[0..*]to salesorder.Item on Item.Parent_ID_ = ID_;
In the Item entity:
_Parent :association[0..1]to salesorder.SalesOrder on _Parent.ID_ = _Parent_ID;
Unfortunately unmanaged associations are not yet available in CDS in HANA SPS8.
The Party is the third entity of the Sales Order business object:
entity Party {
key ID_ :Integer64;
Parent_ID_ :Integer64;
MainIndicator :bo.common.Indicator;
PartyRole :association[0..1]to bo.common.PartyRole;
PartyID_ :Integer64;
Party :association[0..1]to bo.businesspartner.BusinessPartner;
AddressReference :association[0..1]to bo.address.Address;
};
Discussion:
1) Associations from Sales Order to Party and vice versa should be added as unmanaged associations (as discussed for Item).
2) Unmanaged association should be also added to allow direct navigation the Customer, Employee, and other business partners:
Customer :association[0..1]to bo.businesspartner.Customer on Customer.ID_ = PartyID_;
Address
In the address context, we implement the Address entity. This entity shall store addresses for re-use in different business objects.
entity Address {
key ID_ :Integer64;
AddressType :Integer;// TODO code
PreferredCommunicationMediumType :association[0..1]to bo.common.CommunicationMediumType;
GeographicalLocation :address.GeographicalLocation;
// TODO-> [0..*] sub nodes
DefaultName :Name;
DefaultPostalAddress :PostalAddress;
DefaultFacsimile :Telephone;
DefaultConventionalPhone :Telephone;
DefaultMobilePhone :Telephone;
DefaultEmailURI :String(5000);
DefaultWebURI :String(5000);
DefaultWebCode :String(1);// TODO Homepage, Facebook, Twitter, LinkedIn, Google+
DefaultInstantMessagingAccountID :String(5000);
DefaultInstantMessagingCode :String(5000);// TODO: Skype, WhatsApp
Workplace :address.Workplace;
};
Some of the Elements are structured elements, which are defined in the address context, for example:
type PostalAddress {
ScriptCode :String(1);//TODO C = Chinese, I = Latin, K = Japanese, ...
DeliveryServiceTypeCode :String(1);//TODO Street= '1'; POBox = '2'; Company = '3';
Country :association[0..1]to bo.common.Country;
Region :association[0..1]to bo.common.Region {Code};
CountyName :String(40);
CityName :String(40);
DistrictName :String(40);
PostalCode :String(10);
Street :StreetPostalAddress;
POBox :POBoxPostalAddress;
};
Business Partner
Let’s continue with the Business Partner. For our simplified model we implement the following entities:
- Business Partner: We ignore the fact that the business partners name and other common elements may change over time and model them in the BusinessPartner “header” entity.
- Address Information: Sub-Entity with cardinality [0..*] that holds the time- and role-dependent address information of a business partner
- Customer: customer-specific data in the business partner
- Employee, similar to the Customer, not shown
entity BusinessPartner {
key ID_ :Integer64;
ID :String(20);
CategoryCode :businesspartner.CategoryCode;
Person :businesspartner.Person;
Organization :businesspartner.Organization;
UserID :String(255);
Status :association[0..1]to businesspartner.LifeCycleStatus;
};
Discussion
1) Associations to sub-entities should be implemented as discussed for the Sales Order:
Customer :association[0..1] to businesspartner.Customer on Customer.Parent_ID_ = ID_;
AddressInformation :association[0..*] to businesspartner.AddressInformation on
AddressInformation.Parent_ID_ = ID_;
2) A filtered association that points to the current default address should be modelled as unmanaged, filtered association. Filtered associations are also not yet supported by CDS:
CurrentDefaultAddressInformation1 :association[0..*] to businesspartner.AddressInformation on
AddressInformation.Parent_ID_ = ID_ where where ValidityPeriod.StartDate < now() and where ValidityPeriod.EndDate > now() and DefaultIndicator =1;
The Customer entity is implemented as:
entity Customer {
key ID_ :Integer64;
Parent_ID_ :Integer64;
Industry :association[0..1]to bo.common.Industry;
ProspectIndicator :bo.common.Indicator;
BlockingReasons :businesspartner.BlockingReasons;
};
The Address Information entity is implemented as:
entity AddressInformation{
key ID_ :Integer64;
Parent_ID_ :Integer64;
ValidityPeriod :businesspartner.ValidityPeriod;
AddressType :association[0..1]to bo.common.AddressType;
DefaultIndicator :bo.common.Indicator;
AddressUsage :association[0..1]to bo.common.AddressUsage;
// Examples:
// BILL_TO Bill-to party
// SHIP_TO Deliv. address
// EMPP Employee private address
Address :association[0..1]to bo.address.Address;
};
Sales Territory
The Sales Territory is implemented as follows:
entity SalesTerritory {
key ID_ :Integer64;
Name :String(256);
};
entity Customer {
key ID_ :Integer64;
Parent_ID_ :Integer64;
TerritoryAssignmentManualOverrideAllowedIndicator :bo.common.Indicator;
Customer :association[0..1]to bo.businesspartner.Customer;
};
Discussion
As mentioned for the previous discussions, unmanaged associations should be added for navigation from parent to child and vice versa.
The last entity that we implement for our example application is used to store the access rights (or in other words, the restrictions) for a user:
entity RoleAssigmentRestriction {
key ID_ :Integer64;
UserID :String(255);
AccessContextCode :String(5000);
Object_ID :Integer64;
};
A discussion for this entity will follow in the next part of this series.
In the next blog of this series, I will discuss my implementation of the read services (calculated fields, access restriction and OData service definitions).