cancel
Showing results for 
Search instead for 
Did you mean: 

How to get the Entity Key in a Service Handler when following an Association?

nicorunge
Participant
0 Kudos
419

Hi CAP Experts,

I have a task that I thought would be super easy. I have two entities connect via an Association. When calling the associated entity, I want to get the Key in my service handler. Initially I thought I will find this key somewhere in the req object, but I couldn't. Here is my demo project:

bookshop data-model.cds

namespace my.bookshop;

entity Books {
key ID : Integer;
title : String;
stock : Integer;
content : Association to Content @assert.target;
}

entity Content {
key ID : Integer;
content : String
}

cat-service.cds

service CatalogService {
entity Books as projection on my.Books;
entity Content as projection on my.Content;
}

cat-service.js

module.exports = async srv => {
const { Content } = srv.entities

srv.on('READ', Content, (req, next) => {
console.log("What is the current Content ID?")
return next()
})
}

And this is how I call the Content entity via my Books entity:

### Get content via books
GET http://localhost:4004/catalog/Books(201)/content
Content-Type: application/json

I would expect, as I'm successfully following my association, I would also have its key somewhere in my req object. But the only thing I could find is the parents key in req.params. So I could do another select on the parent entity to get the child's key, but as I'm already there, it seems unnecessarily complicated.

Is there a better way? Or am I just blind? 🙂

Thanks!
Nico

Accepted Solutions (1)

Accepted Solutions (1)

vansyckel
Advisor
Advisor
0 Kudos

Hi nicorunge

You are not blind! It isn't there because it cannot be determined without a roundtrip to the database.

req.subject gives you the path in CQN. In your case it should be something like:

{
  "ref": [
    {
      "id": "CatalogService.Books",
      "where": [{ "ref": ["ID"] }, "=", { "val": 201 }]
    },
    "content"
  ]
}

So you can do something like:

const { ID } = await SELECT.one.from(req.subject).columns('ID')

But that's as convenient as it gets due to performance considerations.

Best,
Sebastian

nicorunge
Participant
0 Kudos

Hi vansyckel,

thanks, while checking the req object, I missed the subject. Or at least I did not look at it closely. This query indeed helps to provide the right ID.

Unfortunately, the solution still leads to a somewhat cumbersome coding. Although I want to do the same thing, when calling the Content endpoint directly or when coming via Association, I still have to difference that in my handler. When calling my endpoint directly, I have the ID in req.data.id, when coming via Association I have to do this extra query to get this ID.

I probably don't understand enough how the CAP framework performs this Association under the hood, but wouldn't it make sense to also populate req.data.id when navigation through an Association? In the Book handler, the Content_ID must have been read already to follow the association correctly. Therefore, I could imagine that this could be possible without further database access?!

Best regards,
Nico

vansyckel
Advisor
Advisor
0 Kudos

Hi Nico,

> In the Book handler, the Content_ID must have been read already to follow the association correctly.

Actually, no, in that case we use a subselect to retrieve the correct data, roughly like this:

SELECT * FROM Contents WHERE EXISTS SELECT 1 FROM Books WHERE Books.ID = <UUID> and Books.Content_ID = Contents.ID

I agree that it would be nice to have the info readily available in req.data, but that would mean we'd have to do the database roundtrip every time, even if the information is never needed. Hence, we try to maximize convenience and performance by adding things like req.subject.

Best,
Sebastian

nicorunge
Participant
0 Kudos

Hi Sebastian,

thanks for the explanation! So the entity that is not requested is also not read at all. Of course, this makes perfect sense.
And I absolutely agree that in this case the performance argument is crucial.

Thanks again!
Nico

Answers (1)

Answers (1)

martinstenzig
Contributor

Try the "after" handler and not the "on" handler.

Trulov
Participant
0 Kudos

Or, if you need the ID in the on-handler, you can also await the next-function for an earlier result:

module.exports = async (srv) => {
  const { Content } = srv.entities;

  srv.on('READ', Content, async (req, next) => {
    console.log('What is the current Content ID?');
    const result = await next();
    console.log(`My ID is ${result.ID}!`);
    return result;
  });
};
You then just have to return the result, for the after-handlers.
nicorunge
Participant
0 Kudos

Thank you both for your suggestions. I also thought of both, but couldn't believe that it is necessary.
In reality, I'm not only querying the content entity, but also just this one specific content field, like this:

### Get content via books
GET http://localhost:4004/catalog/Books(201)/content/content
Content-Type: application/json

Like it is done in this media sample. And if just the content field is requested, I exactly do not want to go through the standard handler, because I don't need the data in this case.

Also, the Content handler is of course called directly.

### Get content directly
GET http://localhost:4004/catalog/Content(1)/content
Content-Type: application/json

In this case req.data contains the value {ID:1}. Wouldn't it make sense to have the same data provided when coming from an Association?