Technical Articles
SAP Cloud Platform Backend service: Tutorial [10]: CDS : Associations (3) with redirect
[ERROR] cds.compile failed
Urrggh, what terrible beginning of a tutorial…
Today let’s start with an error message. Please read:
Association “<yourServiceName>.<yourExposedEntity><yourAssociationName>” cannot be implicitly redirected
What???
Just a second, the error message continues:
Target “<yourDefinedEntity>” is exposed in service “<yourServiceName>” by multiple projections: “<yourService>.<yourExposedEntity1>”, “<yourService>.<yourExposedEntity2>”
I don’t understand a word.
Yes, thank you. That’s why you’re here.
I don’t know why I’m here, I’m losing my time in this cabaret
In this blog we’re going to explain the error
<ironic>oh how great…</ironic>
and provide you with the solution
Ah, great ! <muted> I’ll bear the cabaret</muted>
But before we start, here comes the usual necessary link
I know it by heart
for those who don’t know what it is all about: the series of tutorials for new users of SAP Cloud Platform Backend service
Background:
Imagine, you’ve invested lot of effort in order to design a sophisticated model.
Then you happily go to generate an API – but you get the above error message (can be viewed when pressing the “logs” icon)
Of course, you don’t understand a word – we’ve already proven that (see above)
But don’t panic – you’re not alone, you have my blogs.
Experts can jump to the solution
Stage 1: Why define 2 entity sets for one entity type?
Let’ start from the beginning to explain what this is all about.
No, let’s start from the end: you have a web application.
The web application allows to select animals from your pet shop.
Nice, I chose a cat
Of course, everybody wants a cute kitty
Application layer: the web app
Back to your app
Your web application has 2 lists:
– Normal animals
– Dangerous animals
In order to implement this requirement ( 2 lists of animals), the app can do the following (assuming that the app uses an OData service like PetShopService):
1. Normal usage of one service collection with filter
- Call the service for all animals:host…/<service>/animalCollection
- Call the service and filter only dangerous animals:host…/<service>/animalCollection?$filter=isDangerous EQ ‘true’
2. But there’s another possibility:
- Like before, call the same collection to get all animalshost…/<service>/AnimalCollection
- Call a separate collection for only dangerous animalshost…/<service>/DangerousAnimalCollection
This second possibility is more convenient and has probably better performance
But: it has to be implemented by the OData service
Note:
When talking about using an application, we always refer to the runtime.
The app uses a running service to fetch the data
Next layer: the OData service
At runtime, we access an URL, according to the OData service definition.
The URL is composed based on the entity sets, which are exposed by the OData service
As described above, the app has a requirement towards the OData service.
The requirement is:
There should be one data storage for all animals
The service should offer 2 modes of accessing it
More detailed:
The OData service should define 2 entity sets:
<EntitySet Name=”AnimalCollection”/>
<EntitySet Name=”DangerousAnimalCollection”/>
If the app doesn’t define a filter, then how do we get the really dangerous animals, like cats?
Correct, such a requirement must be implemented in the service itself, in the java code (up to now always java code) which is running on the server
It is like a hard-coded filter
Do we want that?
Yes, we want the app to be lightweight, only UI, instead, move most of the logic to the java implementation.
Talking about java implementation, there are (again) 2 possibilities how to implement the filter:
1. Fetch all animals from database, then do the filtering in the java code
2. Fetch only dangerous animals from database
The second possibility is again faster, because no useless data is fetched at all
E.g. using a different SQL statement on the database
This leads us to the next layer:
Next layer: database
Ouh – I always heard that Backend service handles the database?
Good point, we don’t touch any database, but we define the model which leads to the generation of database tables
Remember what we said above:
“one data storage for all animals”
“2 modes of accessing it”
To express this in CDS:
In the data model, we define one entity
In the service definition, we expose 2 entities as projection on the same datamodel-entity
Going further, we can say that the data model is like defining the designtime artifacts, the names of tables and columns to be generated later, during deployment.
And we can say that in the service definition we express how we want the generated service to behave at runtime.
Again:
In the data model, we define one entity
entity AnimalEntity { . . . }
in the service definition, we expose 2 entities as projection on the same datamodel-entity
entity AnimalCollection as projection on AnimalEntity;
entity DangerousAnimalCollection as projection on AnimalEntity;
Next layer: …
There’s no next layer for us.
Summarizing:
We’ve learned why it can make sense to define a model that has one entity exposed by 2 projections.
With other words, defining a service exposing 2 entities that are projections on the same datamodel-entity.
Testing the example
We can go ahead and create an API based on this simple CDS model:
entity AnimalEntity {
key id: Integer;
species : String;
isDangerous : Boolean;
}
service PetShopService {
entity AnimalCollection as projection on AnimalEntity;
entity DangerousAnimalCollection as select from AnimalEntity {*}
where isDangerous = true;
}
Note:
The select statement is explained in the previous tutorial
In order to test, we execute these URLs:
https://…/odatav4/DEFAULT/PETSHOPSERVICE/AnimalCollection
The result: all animals:
If there was no second collection, we would have to do this filter:
https://…/PETSHOPSERVICe/AnimalCollection?$filter=isDangerous EQ true
The result: only dangerous animals:
Last test: the 2nd collection, for convenience:
https://…/PETSHOPSERVICE/DangerousAnimalCollection
As expected, the result payload is the same as in the previous test:
We’re done with this chapter.
Have we learned anything new???
Honestly: no, nothing.
But this chapter was necessary to explain the requirement and to understand the difference between designtime artifacts and runtime behavior
This is prerequisite for understanding the initial ERROR message
Stage 2: Adding associations
Again, in this chapter there won’t be anything new.
Again?? Why are we advancing so slowly?
I like to make slow progress, small steps, but have clear understanding
Now we create a new example model: 2 entities with association
We have a list of “Products”
Each “Product” is supplied by a “Supplier”
A “Supplier” is a “BusinessPartner”
Or: a “Supplier” is a concrete term for a generic “BusinessPartner”
We have an association, which allows us at runtime to navigate from a selected “Product” to its “Supplier” (in OData: NavigationProperty)
This is the model, in its first simple stage:
entity ProductEntity {
key productId: Integer;
productName : String;
linkToSupplier : Association to BusinessPartnerEntity;
}
entity BusinessPartnerEntity {
key id : Integer;
name : String;
type : String; // value can be "Customer" or "Supplier"
}
service SalesOrderService {
entity Products as projection on ProductEntity;
entity BusinessPartners as projection on BusinessPartnerEntity;
}
You may test this API if you like.
Would I learn anything new???
Honestly: no, nothing.
…But?
Honestly: no but
Stage 3: The error
Now we add one more aspect to our model:
The “SalesOrder”
The “SalesOrder” is created for a “Customer”.
Again, a “Customer” is just a special type of “BusinessPartner”
As such, we don’t need a new entity “CustomerEntity” in the data model.
Instead, that’s why the “BusinessPartnerEntity” has property “type”.
The value for this “type” property can be only “Supplier” or ”Customer”, in order to distinguish if the instance at runtime is acting as customer or as supplier.
Note:
This is maybe not the best design, but useful for our tutorial
The “SalesOrder” has an association to the “BusinessPartner” (which is a “Customer”) to allow to navigate from the “SalesOrder” to the “Customer” for whom is was created.
So, we add the following entity to our data model:
entity SalesOrderEntity {
key salesOrderId : Integer;
description : String;
linkToCustomer : Association to BusinessPartnerEntity;
}
Anything new?
Be quiet please
Since we are customer-friendly and keeping in mind that there will be application developers using our API, we directly define 2 projections for the runtime:
One for the “Suppliers”, another one for the “Customers”
Both point to the same entity, the “BusinessPartnerEntity”, both define a filter
entity Customers as select from BusinessPartnerEntity {*}
where type = ‘Customer’;
entity Suppliers as select from BusinessPartnerEntity {*}
where type = ‘Supplier’;
…new?
No, the select statement was introduced in this tutorial
OK, looks all good for us.
But ….
But if we attempt to create an API in Backend service, then we get the infamous ERROR above.
Here’s the full (invalid) model, in case you like to try the error:
entity ProductEntity {
key productId: Integer;
productName : String;
linkToSupplier : Association to BusinessPartnerEntity;
}
entity SalesOrderEntity {
key salesOrderId : Integer;
description : String;
linkToCustomer : Association to BusinessPartnerEntity;
}
entity BusinessPartnerEntity {
key id : Integer;
name : String;
type : String; // value can be "Customer" or "Supplier"
}
service SalesOrderService {
entity Products as projection on ProductEntity ;
entity SalesOrders as projection on SalesOrderEntity;
entity Customers as select from BusinessPartnerEntity {*} where type = 'Customer';
entity Suppliers as select from BusinessPartnerEntity {*} where type = 'Supplier';
}
Explaining the error.
I believe, with what we’ve learned before, it is easy to understand the error.
At designtime (or modeltime), we define an association from “SalesOrderEntity” to “BusinessPartnerEntity”.
At runtime (when running the OData service), we have an instance of “SalesOrders”. It is an EntitySet which points to the “SalesOrders” EntityType
According to the association defined above (SalesOrder -> BusinessPartner) , we want to navigate from one instance of “SalesOrders” to an EntitySet which points to “BusinessPartnerEntity”.
But there are 2 EntitySets pointing to “BusinessPartnerEntity”: “Customers” and “Suppliers”.
Which one is the correct one?
As such, at runtime, there would be an error.
The specification of OData has foreseen such cases.
That’s why OData has the concept of <Association> and <AssociationSet> in OData V2.
One is for defining associations between EntityTypes, the other one is to specify the association between EntitySets.
In OData V4 there are <NavigationProperty> and <NavigationPropertyBinding>, respectively
And now it comes:
CDS has foreseen the same.
And it has a solution: the redirected to statement
One of the differences between OData and CDS is the magic:
In OData you have to always define the <AssociationSet> for an <Association>
In CDS, you need the redirected to statement only if it is needed
CDS is intelligent enough to throw a compilation error, if the redirected to statement is required but missing.
And that’s the initial ERROR.
Anything new?
OK, thanks, all this is new to me
And now, let’s read the error message again:
[ERROR] cds.compile failed:
at petshopModel.cds:22:25-26:
Association “SalesOrderService.Products.linkToSupplier”
cannot be implicitly redirected:
Target “BusinessPartnerEntity” is exposed in service “SalesOrderService” by multiple projections:
“SalesOrderService.Customers”, “SalesOrderService.Suppliers”
at petshopModel.cds:22:25-26:
Association “SalesOrderService.Products.linkToSupplier”
must be redirected:
Target “BusinessPartnerEntity” is not exposed
by service “SalesOrderService”
Cool, I understand
Stage 4: The solution: the redirect statement
This is how it is done:
entity SalesOrders as projection on SalesOrderEntity {
*,
linkToCustomer : redirected to Customers
};
In the projection, we have to declare the association to be redirected to the correct (runtime-) entity.
Moreover, we have to repeat the declaration of all properties, e.g. via asterisk, like done above.
Note:
Don’t forget the asterisk, otherwise you’ll get an error: no key defined.
Reason is that in this kind of definition, you have to list all properties which you want.
Note:
The CDS-validation in the CDS-editor in Visual Studio Code is intelligent enough to throw an error if we specify a redirected to statement pointing to an invalid entity.
The screenshot below shows it:
I’ve specified the “Products” entity, which is existing, but it doesn’t point to “BustinessPartnerEntity”
This is the meaning of the validation error message.
Here’s the full (correct) model:
entity ProductEntity {
key productId: Integer;
productName : String;
linkToSupplier : Association to BusinessPartnerEntity;
}
entity SalesOrderEntity {
key salesOrderId : Integer;
description : String;
linkToCustomer : Association to BusinessPartnerEntity;
}
entity BusinessPartnerEntity {
key id : Integer;
name : String;
type : String; // value can be "Customer" or "Supplier"
}
service SalesOrderService {
entity Products as projection on ProductEntity {
*, // don't forget to add all other properties e.g. via asterisk
linkToSupplier : redirected to Suppliers
};
entity SalesOrders as projection on SalesOrderEntity {
*,
linkToCustomer : redirected to Customers
};
entity Customers as select from BusinessPartnerEntity {*} where type = 'Customer';
entity Suppliers as select from BusinessPartnerEntity {*} where type = 'Supplier';
}
Testing the runtime behavior
We can go ahead and create an API based on the above correct CDS model
This will help us to understand more in depth.
Let’s start with creating a couple of “Suppliers”
{
“id”: 111,
“name”: “supplier1”,
“type”: “Supplier”
}
Create some “Customers”
Note that the id must be different from the created supplier
Why? Have you already forgotten that we modelled: “Suppliers” and “Customers” are redirecting to same table (“BusinessPartnerEntity”)
{
“id”: 222,
“name”: “customer1”,
“type”: “Customer”
}
Create some “Products”.
Note that the value of property “linkToSupplier_id” must be valid. It must be the id of an existing “Supplier”, created before
{
“productId”: 1,
“productName”: “product1”,
“linkToSupplier_id”: 111
}
Create some “SalesOrders” like this:
{
“salesOrderId”: 1,
“description”: “sold some stuff”,
“linkToCustomer_id”: 222
}
Afterwards we can
have a look at the 2 entity sets which point to the same (hidden) “BusinessPartnerEntity” table:
https://…/SERVICE/Suppliers
Result:
And:
https://…/SERVICE/Customers
Result:
Now we can try the navigation:
https://…/SERVICE/SalesOrders(1)/linkToCustomer
Result: we’re taken to the “Customer” which we created before.
And the other navigation
https:///SERVICE/Products(1)/linkToSupplier
Result
The OData metadata
Let’s view how our CDS model and redirects have been translated to OData.
First, we view the metadata in OData version 4
Interesting to note:
Maybe it is a bit surprising that we have 4 EntityTypes instead of 3.
It would have been possible to have one “BusinessPartnerEntity” EntityType and two EntitySets pointing to it (like realized in CDS)
However, for the OData metadata generation, the only relevant input is the CDS service definition.
There, we have 4 entities.
From this perspective it makes sense.
Respectively, in the OData metadata, 2 EntityTypes are identical, guess which ones?
Yawn…
Furthermore, you can see that the CDS Association has a corresponding NavigationProperty in OData.
And you can see that in OData we have always a NavigationPropertyBinding attached to an EntitySet, not only when needed.
Now let’s have a quick view on the OData V2 metadata.
First thing to note:
the metadata document in V2 is longer than the V4
You can see an additional artifact: the Association, defined on same level like EntityType
The Association connects 2 EntityTypes to each other
The EntityType has in addition a NavigationProperty, which points to the Association
Why the NavigationProperty is needed?
Well, it is used when composing the URL, e.g. https:///SERVICE/Products(1)/linkToSupplier
For the runtime part, there’s the additional artifact AssociationSet, it corresponds to the Association and connects 2 EntitySets to each other
This is the OData-V2-way of solving the redirect-problem
One last note
Above, We’ve said that the data for both EntitySets and both EntityTypes is persisted in one table.
When looking at the OData metadata, there’s nowhere any info about one common table for 2 different EntityTypes (“Customers” and “Suppliers”)
But what I said is correct.
There’s only one database table.
Full stop.
Can you prove it?
Yes, I can: 2 proves.
Prove 1:
In the test section above, we’ve created a “Customer” with id 222 and a “Supplier” with id 111
Now try to create a “Supplier” with id 222
It will fail, because that id already exists.
As such: it is proven that we have only one common database table
You can try…
Yes, I’ve tried, correct… I don’t need a second prove…
Prove 2:
As you know, while generating the API, the Backend service application does a compilation step (you can see it in the API Deployment Logs dialog)
This compilation generates the OData metadata files (resp. edmx files for OData v4 and v2)
But this compilation also generates the descriptor files for the HANA DB (currently the underlying database)
If you could look at these files, you would see that there’s
one entity BUSINESSPARTNERENTITY,
but 2 views SALESORDERSERVICE_CUSTOMERS and SALESORDERSERVICE_SUPPLIERS, both views pointing to the same entity
In total, there are 3 entities and 4 views.
The Java implementation of the OData service works on the views, so everything is fine.
How can I verify this prove?
If you’re lucky, I will show you in a future tutorial how to view these hidden artifacts.
When?
Don’t know
Then this prove is not valid
Yes it is
No
Yes
No
Enough, we still have prove 1
OK ok
One (more) last note
We know that CDS is powerful and intelligent…
From what we read in the initial ERROR message, we assume that in the normal case, the redirect is done implicitly by CDS
Pure magic
In normal associations, if we don’t define a “redirected to”, then it is still done implicitly by CDS
This means, we should be able to write it, even if not required.
So tet’s try to make it explicit, then see what happens.
This example (based on the example in stage 2) shows it:
entity ProductEntity {
key productId: Integer;
productName : String;
linkToSupplier : Association to BusinessPartnerEntity;
}
entity BusinessPartnerEntity {
key id : Integer;
name : String;
type : String;
}
service SalesOrderService {
entity Products as projection on ProductEntity{
*,
linkToSupplier : redirected to BusinessPartners
};
entity BusinessPartners as projection on BusinessPartnerEntity;
}
Note:
the explicit redirect is not necessary, because:
In the above model, we don’t have 2 projections on same entity, involved with association
You can create an API based on this model and you’ll see that it works, it compiles and works exactly same way like the example without explicit redirect. (stage 2)
Appendix 1: The solution
I like to have an appendix with the full sample model.
So here it is:
entity ProductEntity {
key productId: Integer;
productName : String;
linkToSupplier : Association to BusinessPartnerEntity;
}
entity SalesOrderEntity {
key salesOrderId : Integer;
description : String;
linkToCustomer : Association to BusinessPartnerEntity;
}
entity BusinessPartnerEntity {
key id : Integer;
name : String;
type : String; // value can be "Customer" or "Supplier"
}
service SalesOrderService {
entity Products as projection on ProductEntity {
*, // don't forget to add all other properties
linkToSupplier : redirected to Suppliers
};
entity SalesOrders as projection on SalesOrderEntity {
*,
linkToCustomer : redirected to Customers
};
entity Customers as select from BusinessPartnerEntity {*} where type = 'Customer';
entity Suppliers as select from BusinessPartnerEntity {*} where type = 'Supplier';
}
Appendix 2: The apology
Sorry, I forgot to mention that the naming I chose for the CDS elements are not recommendable. I chose those names only for better understanding.
You shouldn’t use naming like “linkToAnything” or ending with suffix “…Entity”
Appendix 3: The pet
I feel like I owe you another appendix with another kitty….
Hello Carlos,
Thank you for the well-explained blog.
Is there a documentation for CDS associations with redirected to syntax? If so, could you please provide it?
The SAP Help Platform does not seem to contain it.(Core Data and Services (CDS) Language Reference
Hello Raluca Placinta
Thanks very much for your friendly Feedback!
The new CAP documentation has been recently published, you should use this link:
https://cap.cloud.sap/
Kind Regards,
Carlos
Great blog post Carlos Roggan .
I was trying to build a fiori elements list report on top of a CAP service and was hitting these errors you have explained when I tried to create a filter field (value help) that showed all the unique values of a property in the main EntitySet.
To do this effectively means your CAP service needs to expose the entity twice - one for the main list report table and again for the value help to find unique values.
I was hitting these errors which you've explained can be solved with the redirects. Awesome!