SAP Tutorial: Complete CAP Java Part 7

Brian Heise
10 min readJul 16, 2021

--

Aspects, Custom Types, and Associations in CDS data models

Photo by Glenn Carstens-Peters on Unsplash

Contents

Welcome to Part 7 of the Complete CAP Java tutorial series. Week by week I’m showing you how to rebuild SAP’s CAP Java sample application, an app for running an online bookshop. Up to now we’ve been implementing the customer-facing Browse Books application. The two images below show what we have made so far and what our target looks like:

SAP’s Browse Books app

In this and the next few installments we’re going to focus on the Add Review button. There’s a lot to implement here — a new data model, a custom oData action, custom Java logic, and the UI itself. Buckle in!

Step 1: The Ratings Data Model

Take a look at the Add Review popover from SAP’s example app (you can display this by clicking the Add Review button):

Add Review Popover

It looks like our reviews will have three displayed fields — Rating, Title, and Text. By now you should know how to implement these so I won’t go into details. If you need a refresher, check out the Part 1 of this series. I’ll show my code below, but as a challenge try implementing it yourself before scrolling down.

// db/Reviews.cdsnamespace toadslop.bookshop;entity Reviews {
key id : Integer;
rating : Integer;
title : String(100);
text : String(1000);
}

This is pretty good and it will get the job done but we can add some extra features to make it more robust.

Universal Unique Identifiers

In the first place there’s the ID. Of course we can just use a simple integer and increment it with each new review as we did with the Books, but thanks to CAP we can do better than that: with almost no effort we can implement CUIDs, a CAP’s flavor of Universal Unique Identifiers. All we have to do is import the cuid cds aspect as shown below.

// db/Reviews.cdsnamespace toadslop.bookshop;using {cuid} from '@sap/cds/common';entity Reviews : cuid {
rating : Integer;
title : String(100);
text : String(1000);
}

It’s that simple. Just import cuid from @sap/cds/common and add it to your model as shown above, then delete the original id field. What this does behind the scenes is add a key field called id to your model. CAP will automatically generate a CUID and insert it into this field whenever you create a new entity. It’s super convenient. While we’re at it, let’s go and add cuid to our Books model as well.

Created At, Updated At, Created By, Updated By

The next thing we might care about is which user created a review and when. Luckily, CAP provides us with a built-in, automatic feature for this too: the managed aspect. Let’s import that and add it to our model as well.

// db/Reviews.cdsnamespace toadslop.bookshop;using {cuid, managed} from '@sap/cds/common';entity Reviews : cuid, managed {
rating : Integer;
title : String(100);
text : String(1000);
}

And there it is: now whenever a Review is created or updated, who did the creating or updating and when will automatically be recorded. We don’t have to do anything else!

For more information on these and other cds aspects, be sure to check out this documentation.

Associations

The next thing that we need to do is to associate our Reviews with our Books. Since each book can have many reviews and each review can have only one book, we need this relationship captured by our data model. Conventionally we would have to set up foreign keys and all kinds of validations for this, but again CAP is there to help us out: we can easily create associations with almost no effort at all. CAP will handle all the details behind the scenes. Check the code below:

// db/Reviews.cdsnamespace toadslop.bookshop;using {toadslop.bookshop as bookshop} from './index';
using {
cuid,
managed
} from '@sap/cds/common';
entity Reviews : cuid, managed {
book : Association to bookshop.Books;
rating : Integer;
title : String(100);
text : String(1000);
}

Notice how we first import our toadslop.bookshop namespace from index.cds, which gives us access to our Books model (recall that we imported our Books model there for easy access). After that we simply add a field called book to our model and define it as Association to bookshop.Books. By using this Association keyword CAP will generate a field called BOOK_ID behind the scenes and fill it with the associated book’s ID automatically when a new review is created. Yet again, all the technical details are handled for us. However, we also need to go back to our book model and add the association there as well.

First, import our reviews model in index.cds for convenient importing.

// db/index.cdsusing from './books';
using from './reviews';

Then import the reviews model in books.cds and add the association as shown below:

// db/books.cdsnamespace toadslop.bookshop;using {toadslop.bookshop as bookshop} from './index';// ... abridgedentity Books : cuid {
// ... abridged
reviews : Association to many bookshop.Reviews
on reviews.book = $self;
}

This association is a bit more complex than the other so let’s break it down. We first define a field called reviews. Next we define it as an Association to many bookshop.Reviews. With this definition CAP will know not to create a field in the database — it won’t exist there at all. Instead, the CAP runtime will generate some logic for how to retrieve the list of reviews associated with a specific Books entity so that when we want to retrieve the reviews for a specific book we can easily do it without needing to write any SQL. Finally we provide the condition: when the book field of a Reviews entity contains the ID of the current Books entity, it will be included.

Enum Types

Next, the rating field itself. We just marked it as an Integer here unlike the Rating field in the Books model, which is a Decimal. This is because Books is an average of many single ratings from Reviews so a Books average might come out as a decimal, but we don’t want our users to enter anything but whole numbers. Of course, the number that the user enters should be between 0 and 5, so at first you might think we should use the @assert.range annotation on again. Indeed we could do that and it would work fine, but we could do a little better. The reason is that each rating number actually has a semantic meaning. By using CDS’s Enum Types we can explicitly define the semantics. We can define the type as shown below:

// db/rating.cdsnamespace toadslop.bookshop;type Rating : Integer enum {
Great = 5;
Good = 4;
Average = 3;
Poor = 2;
Bad = 1;
Terrible = 0;
}

Now we can import this in our Reviews model and set this as the type for the rating field as shown below:

// db/Reviews.cdsentity Reviews : cuid, managed {
book : Association to bookshop.Books;
rating : bookshop.Rating;
title : String(100);
text : String(1000);
}

Validations

Finally, we have one last thing to do: validations. Here we will use @mandatory for title and text because we want to make sure our users always include those. Finally, for rating we add @assert.range like we used with our rating field in the Books entity. The difference here is that we don’t have to manually specify the range — because we’re using an enum type that’s handled for us.

// db/Reviews.cdsentity Reviews : cuid, managed {
book : Association to bookshop.Books;
rating : bookshop.Rating @assert.range;
title : String(100) @mandatory;
text : String(1000) @mandatory;
}

All right! Now we have our Reviews model defined and ready to go. Next we’ll tackle providing a bit of mock data for the Reviews and to update our Books mock data to use cuids instead of simple integers.

Step 2: Mock Data Revisited

Now that we have our model ready, we need to provide some mock data to help us when we’re working with the UI. If you recall from back in Part 2, we need to provide a semicolon-separated csv file in the folder db/data and the file should be named with this pattern: <namespace>-<entity>.csv. Let’s make that file.

In the first line of the file, we need the names of the fields as they are in the database, not as we defined them in our CDS files. An easy way to check them is to run the command cds compile ./db/Reviews.cds --to hana and see what comes out.

Using this, we can set up our CSV header like so:

// db/data/toadslop.bookshop-Reviews.cdsID;CREATEDAT;CREATEDBY;MODIFIEDAT;MODIFIEDBY;RATING;TITLE;TEXT;BOOK_ID;

Notice that we did not include a field for BOOK. That’s because this field doesn’t exist as a normal field in the database. BOOK_ID is all we need to concern ourselves with.

The next steps are unfortunately a bit tedious as CAP doesn’t currently provide a build-in way to generate data, though I suspect one could be built without too much trouble using a library like Faker, but for now we’ll just do it manually.

First, let’s generate some IDs. Because we’re now using CUIDs, we can’t just put in an integer anymore. However developer Andrew Barnard describes a nice way to get this done automatically in this blog post. While we’re at it, we do need to update the IDs in our Books model too since we switched those over to CUIDs. You can go ahead and try to do this all yourself, but as it’s tedious and we’re really more interested in learning the CAP framework, you can also just copy the sample data from SAP’s Bookshop app repository and move on to the next step. Be careful about copying the Books data, though! They have some fields that we haven’t implemented yet, so make sure you don’t copy those — just update the IDs.

Step 3: Adding the New Model to Our Service

Now that we have our new model and some mock data, let’s add it to our oData service so that we can access it. If you’ve been following this series up to this point, you should know how to do this so give it a try on your own. After that, scroll down and check how I did it.

Not bad! Now we need to test it.

Step 4: API Testing Revisited

For testing our new model in the service we need to do a bit more than we did with the Book model back in Part 1 because now we have two models with an association to each other, so we need to test that association. Check the code below for my tests.

### Get all Reviews
GET http://admin:admin@localhost:8080/api/browse/Reviews HTTP/1.1
### Create a Review
POST http://admin:admin@localhost:8080/api/browse/Reviews HTTP/1.1
content-type: application/json
{
"title": "I hated it",
"text": "Birds freak me out",
"rating": 1,
"book_ID": "51061ce3-ddde-4d70-a2dc-6314afbcc73e"
}
### Get a Review
GET http://admin:admin@localhost:8080/api/browse/Reviews(8089768a-14ae-3cd0-807e-c77ceab8f91e) HTTP/1.1
### Update a review
PATCH http://admin:admin@localhost:8080/api/browse/Reviews(8089768a-14ae-3cd0-807e-c77ceab8f91e) HTTP/1.1
content-type: application/json
{
"rating": 2
}
### Delete a rating
DELETE http://admin:admin@localhost:8080/api/browse/Reviews(8089768a-14ae-3cd0-807e-c77ceab8f91e) HTTP/1.1
content-type: application/json
### Get all reviews for a book
GET http://admin:admin@localhost:8080/api/browse/Books(f846b0b9-01d4-4f6d-82a4-d79204f62278)/reviews HTTP/1.1
content-type: application/json
### Get a single review through a book
GET http://admin:admin@localhost:8080/api/browse/Books(f846b0b9-01d4-4f6d-82a4-d79204f62278)/reviews(8098ea0a-e4b9-3265-9a21-95758a1e49e0) HTTP/1.1
content-type: application/json
### Create a review through a book
POST http://admin:admin@localhost:8080/api/browse/Books(f846b0b9-01d4-4f6d-82a4-d79204f62278)/reviews HTTP/1.1
content-type: application/json
{
"title": "I hated it",
"text": "Birds freak me out",
"rating": 1
}

Of these tests, the last few are most interesting: without any work writing controller code the CAP framework already has not just the CRUD actions ready to go, as we’ve seen before, but the association also fully works. We didn’t even need to define any routes. Amazing right? The power of CAP is that we only need custom code when we really need custom logic — standard CRUD operations and associations are built-in.

Conclusion

In this week’s installment we implemented a Ratings model and in the process learned some more features of CDS that we can use to quickly and easily make our models more robust, such as the cuid and managed aspects, enum types, and associations. We also updated our mock data and tested out the associations that we defined in the model. Next week will will implement the custom logic we need to handle creating our Reviews and implement a button in the UI to trigger that logic. Until then, take care!

Was anything unclear in this tutorial? Leave a question below and I’ll get back to you as soon as possible. Was anything incorrect? Please leave a comment below and let me know (a source for the correction would be most helpful). Thanks for your comments!

--

--

Brian Heise
Brian Heise

Written by Brian Heise

Full Stack web developer employed at Liferay Japan

Responses (1)