How to specify which fields to return with #query anotation in Spring Data? - spring-data-rest

When I run the following query using the #query anotation in Spring Boot, it returns the correct result:
SELECT p FROM Collection p WHERE LOWER(p.description) LIKE LOWER(CONCAT('%',:searchTerm, '%'))
{
"_embedded": {
"collections": [
{
"place": "Blessington",
"description": "Collection of old shoes for recycling",
"_links": {
"self": {
"href": "http://localhost:8080/collections/1"
},
"collection": {
"href": "http://localhost:8080/collections/1"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/collections/search/findByDescription?searchTerm=shoe"
}
}
}
When I try to specify the fields to return:
SELECT p.description FROM Collection p WHERE LOWER(p.description) LIKE LOWER(CONCAT('%',:searchTerm, '%'))
I get the following error:
{
"cause": null,
"message": "PersistentEntity must not be null!"
}
How do I specify which fields to return with #query annotation in Spring Data?

Yes it seems the question that Manish posted the link to has the answer.
Answer: You can't.
Spring data will return the whole entity, not individual fields. You can't make it do that. If you want to do that you have to use projections. See linked post.
Thanks #Manish

The suggested link does not fully cover all possibilities.
From the Hopper release of Spring Data it is possible to directly return Projections directly from Query methods:
https://spring.io/blog/2016/05/03/what-s-new-in-spring-data-hopper#projections-on-repository-query-methods
So you can therefore do:
public interface ThingRepository extends JpaRepository<Thing, Long>{
#Query("select t from Thing t .....")
public List<ThingProjection> findBySomeCriteria(...);
}

Related

Use a projection to modify only one field but keeping default for all others

First of all I'd like to say I love what i've seen so far from Spring Data JPA and Spring Data REST. Thanks a lot to all people involved.
Problem description
I have an entity model similar to the classes below. One parent and two different child entities referencing the parent als a ManyToOne Assoziation. For one of the childs i like to have the default rendering of all its properites and links as it is when no projection is applied to the parent.
The other child should be mapped to a simple string array containing only the id or some specific field.
Code and example JSONs
#Entity
public class Parent {
#Id
private Long id;
private String parentValue;
#OneToMany(mappedBy = "parent")
private List<Child1> child1;
#OneToMany(mappedBy = "parent")
private List<Child2> child2;
// ... getters and setters
}
#Entity
public class Child1 {
#Id
private Long id;
private String child1Value;
#ManyToOne
Parent parent;
// ... getters and setters
}
#Entity
public class Child2 {
#Id
private Long id;
#ManyToOne
Parent parent;
}
the response when getting the collection resource of parent is this
{
"_embedded": {
"parents": [
{
"parentValue": "Parent1",
"child1": [
{
"child1Value": "Child1",
"_links": {
"parent": {
"href": "http://localhost:8080/parents/1"
}
}
}
],
"child2": [
{
"_links": {
"parent": {
"href": "http://localhost:8080/parents/1"
}
}
}
],
// removed remaining json to shorten the example
}
But what i like to achieve is the following JSON
{
"_embedded": {
"parents": [
{
"parentValue": "Parent1",
"child1": [
{
"child1Value": "Child1",
"_links": {
"parent": {
"href": "http://localhost:8080/parents/1"
}
}
}
],
"child2": [1],
What i tried so far
Added an excerptProjection to the ParentRepository:
#RepositoryRestResource(excerptProjection = ParentRepository.ArrayProjection.class)
public interface ParentRepository extends PagingAndSortingRepository<Parent, Long>{
public interface ArrayProjection {
String getParentValue();
List<Child1> getChild1();
#Value("#{target.child2.![id]}")
List<Long> getChild2();
}
}
Edited: In the first version of the question, the Projection was incorrect regarding the return type of getChild1(), as it should return the complete collection not only one element. Thanks #kevvvvyp for still trying to help.
The result is similar to what i want, but the links on the Child1 property are missing now:
{
"_embedded": {
"parents": [
{
"child2": [
2
],
"child1": {
"child1Value": "Child1"
},
"parentValue": "Parent1",
// removed remaining json to shorten example
Also the approach with the excerptProjection means i'd have to change the projection everytime the entity changes. Which probalby won't happen to much but that means somebody will forget to change it in the future ;-)
I think a projection is the right way to go here, what about...
interface ArrayProjection {
#JsonIgnore
#Value("#{target}")
Parent getParent();
default String getParentValue(){
return this.getParent().getParentValue();
}
default Child1 getChild1(){
//TODO what are we actually trying to return here? Given we join the parent to
// the child on id & id is the primary key on both entities can we ever get more than one child?
return CollectionUtils.firstElement(this.getParent().getChild1());
}
default List<Long> getChild2() {
return this.getParent().getChild2().stream()
.map(Child2::getId)
.collect(Collectors.toList());
}
}
Response...
GET http://localhost:8080/api/parents
HTTP/1.1 200
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 31 Mar 2021 21:08:54 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"_embedded": {
"parents": [
{
"parentValue": "Parent1",
"child1": {
"child1Value": "Child1"
},
"child2": [
1
],
"_links": {
"self": {
"href": "http://localhost:8080/api/parents/1"
},
"parent": {
"href": "http://localhost:8080/api/parents/1{?projection}",
"templated": true
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/api/parents"
},
"profile": {
"href": "http://localhost:8080/api/profile/parents"
}
},
"page": {
"size": 20,
"totalElements": 1,
"totalPages": 1,
"number": 0
}
}
Response code: 200; Time: 713ms; Content length: 724 bytes
In reponse to your concern, if we code using default method's we will see a compile error when that entity changes. It might be possible to use a class projection instead.
Also I would consider if we really want to return a projection by default... it might confuse a client who then tries to create/update a parent (this is perhaps why you've kept the id's hidden by default?).
Sample - https://github.com/kevvvvyp/sdr-sample
Update
Another (more complex) way to acheive this could be to make use of Jackson's SimpleBeanPropertyFilter, you could customise the JSON response based on some value (the entity class, a value in a particular field, an annotation etc).

how to select a single item and get it's relations in faunadb?

I have two collections which have the data in the following format
{
"ref": Ref(Collection("Leads"), "267824207030650373"),
"ts": 1591675917565000,
"data": {
"notes": "voicemail ",
"source": "key-name",
"name": "Glenn"
}
}
{
"ref": Ref(Collection("Sources"), "266777079541924357"),
"ts": 1590677298970000,
"data": {
"key": "key-name",
"value": "Google Ads"
}
}
I want to be able to query the Leads collection and be able to retrieve the corresponding Sources document in a single query
I came up with the following query to try and use an index but I couldn't get it to run
Let(
{
data: Get(Ref(Collection('Leads'), '267824207030650373'))
},
{
data: Select(['data'],Var('data')),
source: q.Lambda('data',
Match(Index('LeadSourceByKey'), Get(Select(['source'], Var('data') )) )
)
}
)
Is there an easy way to retrieve the Sources document ?
What you are looking for is the following query which I broke down for you in multiple steps:
Let(
{
// Get the Lead document
lead: Get(Ref(Collection("Leads"), "269038063157510661")),
// Get the source key out of the lead document
sourceKey: Select(["data", "source"], Var("lead")),
// use the index to get the values via match
sourceValues: Paginate(Match(Index("LeadSourceValuesByKey"), Var("sourceKey")))
},
{
lead: Var("lead"),
sourceValues: Var("sourceValues")
}
)
The result is:
{
lead: {
ref: Ref(Collection("Leads"), "269038063157510661"),
ts: 1592833540970000,
data: {
notes: "voicemail ",
source: "key-name",
name: "Glenn"
}
},
sourceValues: {
data: [["key-name", "Google Ads"]]
}
}
sourceValues is an array since you specified in your index that there will be two items returned, the key and the value and an index always returns the array. Since your Match could have returned multiple values in case it wasn't a one-to-one, this becomes an array of an array.
This is only one approach, you could also make the index return a reference and Map/Get to get the actual document as explained on the forum.
However, I assume you asked the same question here. Although I applaud asking questions on stackoverflow vs slack or even our own forum, please do not just post the same question everywhere without linking to the others. This makes many people spend a lot of time while the question is already answered elsewhere.
You might probably change the Leads document and put the Ref to Sources document in source:
{
"ref": Ref(Collection("Leads"), "267824207030650373"),
"ts": 1591675917565000,
"data": {
"notes": "voicemail ",
"source": Ref(Collection("Sources"), "266777079541924357"),
"name": "Glenn"
}
}
{
"ref": Ref(Collection("Sources"), "266777079541924357"),
"ts": 1590677298970000,
"data": {
"key": "key-name",
"value": "Google Ads"
}
}
And then query this way:
Let(
{
lead: Select(['data'],Get(Ref(Collection('Leads'), '267824207030650373'))),
source:Select(['source'],Var('lead'))
},
{
data: Var('lead'),
source: Select(['data'],Get(Var('source')))
}
)

Make collection propertie render as relation instead of property in json HAL representaion

I got hal formatted response as this:
{
"name": "Publisher A",
"bookPublishers": [
{
"publishedDate": "2019-07-12T08:19:04.583+0000",
"_links": {
"publisher": {
"href": "http://localhost:8080/api/publishers/1"
},
"book": {
"href": "http://localhost:8080/api/books/2"
}
}
},
{
"publishedDate": "2019-07-12T08:19:04.564+0000",
"_links": {
"publisher": {
"href": "http://localhost:8080/api/publishers/1"
},
"book": {
"href": "http://localhost:8080/api/books/1"
}
}
}
],
"_links": {
"self": {
"href": "http://localhost:8080/api/publishers/1"
},
"publisher": {
"href": "http://localhost:8080/api/publishers/1"
},
"friends": {
"href": "http://localhost:8080/api/publishers/1/friends"
},
"createdBy": {
"href": "http://localhost:8080/api/publishers/1/contact"
}
}
}
I see there property bookPublishers and also in links friends. Imho they should be both association links (see 2.4. Creating the Associations) where can I "put" another resources.
I would like to make spring render bookPublishers same as friends.
Sample project is here: https://github.com/luvarqpp/poc-springHalRelations
You can do:
git clone https://github.com/luvarqpp/poc-springHalRelations.git
cd poc-springHalRelations
mvn clean spring-boot:run
And than open http://localhost:8080/api
PS: Bonus question, what is easiest way to provide own relation for business logic, like relation "renameAuthor" for example.
For collection relationships, Spring Data will provide a link when a repository exists for the relevant type. Where no repository exists then the collection will be in-lined in the response, otherwise, how else will the client get the data.
Therefore, create a repository for your BookPublisher type.
Relevant documentation part citation:
the component responsible for creating the links to referenced entities (such as those objects under the _links property in the object’s JSON representation). It takes an #Entity and iterates over its properties, creating links for those properties that are managed by a Repository and copying across any embedded or simple properties.
You can also create a projection that would in-line the data when required. Clients could specify this projection in the request therefore preventing an additional server call.
e.g.
/publisher/1?projection=withBookPublishers.
https://docs.spring.io/spring-data/rest/docs/current/reference/html/#projections-excerpts.projections

MarkLogic - Xpath on JSON document

MarkLogic Version: 9.0-6.2
I am trying to apply Xpath in extract-document-data (using Query Options) on a JSON document shown below. I need to filter out "Channel" property if the underneath property "OptIn" has a value of "True".
{
"Category":
{
"Name": "Severe Weather",
"Channels":[
{
"Channel":
{
"Name":"Email",
"OptIn": "True"
}
},
{
"Channel":
{
"Name":"Text",
"OptIn": "False"
}
}
]
}
}
I tried below code,
'<extract-document-data selected="include">' +
'<extract-path>//*[OptIn="True"]/../..</extract-path>' +
'</extract-document-data>' +
which is only pulling from "Channel" property as shown below.
[
{
"Channel": {
"Name": "Email",
"OptIn": "True"
}
}
]
But my need is to pull from parent "Category" property, but filter out the Channels that have OptIn value as False.
Any pointers?
If I understand correctly, you'd like to extract 'Category', but only with those 'Channel's that have 'OptIn' equalling 'true', right?
Extract-document-data is not advanced enough for that. You best extract entire Categories which have at least one OptIn equalling true (//Category[//OptIn = 'true']), and use a REST transform on the search response to trim down the unwanted Channels..
HTH!

Is this API structure HATEOAS compatible?

I'm wondering if the following structure of API links is HATEOAS compatible?
Especially I'm not sure of /create endpoint. Should it be at the entry level because user can create a group from there or is it fine to put it in /groups?
What are your thoughts about the rest? It should be also HAL compatible.
/groups
/create
/detail/{groupId}
/update
/delete
/items
/search{?page,size,sort}
HATEOAS (see Richardson's Maturity Model level 3) is all about links, so with HAL Browser this would look something like this:
Root:
{
"_links": {
"self": {
"href": "/api/root"
},
"api:group-add": {
"href": "http://apiname:port/api/group"
},
"api:group-search": {
"href": "http://apiname:port/api/group?pageNumber={pageNumber}&pageSize={pageSize}&sort={sort}"
},
"api:group-by-id": {
"href": "http://apiname:port/api/group/id" (OR "href": "http://apiname:port/api/group?id={id}")
}
}
}
The add would simply be a POST to that endpoint, and then you'd have 2 GET methods.
Then once you drill down to a particular group (say #1):
{
"Id" : 1,
"Name" : "test",
"_links": {
"self": {
"href": "/api/group/1" (OR "/api/group?id=1")
},
"edit": {
"href": "http://apiname:port/api/group/1"
},
"api:delete": {
"href": "http://apiname:port/api/group/1"
},
"api:items-query": {
"href": "http://apiname:port/api/bonus?groupId=1"
}
}
}
Here, the edit would simply be a PUT, and then you'll need a DELETE (see level 2 of REST in that same link), as for the items, you probably know best if they are just a property, or another endpoint; you could even embed them to be returned in the same call that's retrieving a group.