I have a database service using Spring Boot 1.5.1 and Spring Data Rest. I am storing my entities in a MySQL database, and accessing them over REST using Spring's PagingAndSortingRepository. I found this which states that sorting by nested parameters is supported, but I cannot find a way to sort by nested fields.
I have these classes:
#Entity(name = "Person")
#Table(name = "PERSON")
public class Person {
#ManyToOne
protected Address address;
#ManyToOne(targetEntity = Name.class, cascade = {
CascadeType.ALL
})
#JoinColumn(name = "NAME_PERSON_ID")
protected Name name;
#Id
protected Long id;
// Setter, getters, etc.
}
#Entity(name = "Name")
#Table(name = "NAME")
public class Name{
protected String firstName;
protected String lastName;
#Id
protected Long id;
// Setter, getters, etc.
}
For example, when using the method:
Page<Person> findByAddress_Id(#Param("id") String id, Pageable pageable);
And calling the URI http://localhost:8080/people/search/findByAddress_Id?id=1&sort=name_lastName,desc, the sort parameter is completely ignored by Spring.
The parameters sort=name.lastName and sort=nameLastName did not work either.
Am I forming the Rest request wrong, or missing some configuration?
Thank you!
The workaround I found is to create an extra read-only property for sorting purposes only. Building on the example above:
#Entity(name = "Person")
#Table(name = "PERSON")
public class Person {
// read only, for sorting purposes only
// #JsonIgnore // we can hide it from the clients, if needed
#RestResource(exported=false) // read only so we can map 2 fields to the same database column
#ManyToOne
#JoinColumn(name = "address_id", insertable = false, updatable = false)
private Address address;
// We still want the linkable association created to work as before so we manually override the relation and path
#RestResource(exported=true, rel="address", path="address")
#ManyToOne
private Address addressLink;
...
}
The drawback for the proposed workaround is that we now have to explicitly duplicate all the properties for which we want to support nested sorting.
LATER EDIT: another drawback is that we cannot hide the embedded property from the clients. In my original answer, I was suggesting we can add #JsonIgnore, but apparently that breaks the sort.
I debugged through that and it looks like the issue that Alan mentioned.
I found workaround that could help:
Create own controller, inject your repo and optionally projection factory (if you need projections). Implement get method to delegate call to your repository
#RestController
#RequestMapping("/people")
public class PeopleController {
#Autowired
PersonRepository repository;
//#Autowired
//PagedResourcesAssembler<MyDTO> resourceAssembler;
#GetMapping("/by-address/{addressId}")
public Page<Person> getByAddress(#PathVariable("addressId") Long addressId, Pageable page) {
// spring doesn't spoil your sort here ...
Page<Person> page = repository.findByAddress_Id(addressId, page)
// optionally, apply projection
// to return DTO/specifically loaded Entity objects ...
// return type would be then PagedResources<Resource<MyDTO>>
// return resourceAssembler.toResource(page.map(...))
return page;
}
}
This works for me with 2.6.8.RELEASE; the issue seems to be in all versions.
From Spring Data REST documentation:
Sorting by linkable associations (that is, links to top-level resources) is not supported.
https://docs.spring.io/spring-data/rest/docs/current/reference/html/#paging-and-sorting.sorting
An alternative that I found was use #ResResource(exported=false).
This is not valid (expecially for legacy Spring Data REST projects) because avoid that the resource/entity will be loaded HTTP links:
JacksonBinder
BeanDeserializerBuilder updateBuilder throws
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of ' com...' no String-argument constructor/factory method to deserialize from String value
I tried activate sort by linkable associations with help of annotations but without success because we need always need override the mappPropertyPath method of JacksonMappingAwareSortTranslator.SortTranslator detect the annotation:
if (associations.isLinkableAssociation(persistentProperty)) {
if(!persistentProperty.isAnnotationPresent(SortByLinkableAssociation.class)) {
return Collections.emptyList();
}
}
Annotation
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
public #interface SortByLinkableAssociation {
}
At project mark association as #SortByLinkableAssociation:
#ManyToOne
#SortByLinkableAssociation
private Name name;
Really I didn't find a clear and success solution to this issue but decide to expose it to let think about it or even Spring team take in consideration to include at nexts releases.
Please see https://stackoverflow.com/a/66135148/6673169 for possible workaround/hack, when we wanted sorting by linked entity.
Related
I have a requirement to mark certain properties in my REST beans as ignored using #JsonIgnore. (I am using Spring Boot). This helps in avoiding these properties in my Swagger REST documentation.
I also would like to ensure that if the client passes these properties, an error is sent back. I tried setting spring.jackson.deserialization.fail-on-unknown-properties=true, but that works only for properties that are truly unknown. The properties marked with #JsonIgnore passes through this check.
Is there any way to achieve this?
I think I found a solution -
If I add #JsonProperty(access = Access.READ_ONLY) to the field that is marked as #JsonIgnore, I get back a validation error. (I have also marked the property with #Null annotation. Here is the complete solution:
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Employee {
#Null(message = "Id must not be passed in request")
private String id;
private String name;
//getters and setters
}
#JsonInclude(JsonInclude.Include.NON_NULL)
public class EmployeeRequest extends Employee {
#Override
#JsonIgnore
#JsonProperty(access = Access.READ_ONLY)
public void setId(String id) {
super.setId(id);
}
}
PS: By adding #JsonProperty(access = Access.READ_ONLY), the property started showing up in Swagger model I had to add #ApiModelProperty(hidden = true) to hide it again.
The create method takes EmployeeRequest as input (deserialization), and the get method returns Employee as response (serialization). If I pass id in create request, with the above solution, it gives me back a ConstraintViolation.
PS PS: Bummer. None of these solutions worked end-to-end. I ended up creating separate request and response beans - with no hierarchical relationship between them.
My Repository Method Query cannot parse a property name that comes after the OrderBy, but it can if it follows the findBy or findAllBy. The attribute in my entity that is giving me issues is zIndex
Entity Class
#Entity
public class DisplayLayer
{
#Id
#Column(name="DISPLAY_LAYER_ID")
private Long id;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name="DISPLAY_CONTAINER_ID")
private DisplayContainer displayContainer;
#Column(name="NAME")
private String name;
#Column(name="Z_INDEX")
private Long zIndex;
#Column(name="DESCRIPTION")
private String description;
// Getters & Setters
}
Repository Class
public interface DisplayLayerRepository extends BaseRepository<DisplayLayer, Long>
{
// This one method query works just fine
public List<DisplayLayer> findByZIndex(Long pZIndex);
// These two throw the same error:
// Unable to locate Attribute with the the given name [ZIndex]
//
// public List<DisplayLayer> findAllByOrderByZIndexAsc();
// public List<DisplayLayer> findByDisplayContainerIdOrderByZIndexAsc(Long pDisplayLayerId);
}
So in my first method query, when OrderBy is not used, it correctly parses it as [zIndex], but when it follows the OrderBy for some reason it capitalizes the z: [ZIndex].
Is this a known issue, or am I doing something wrong?
EDIT
What makes me think there might be a bug with the parser is that if I update zIndex to IndexZ in my entity and then change my query methods from OrderByZIndex to OrderByIndexZ everything works.
This solution is not ideal though as the database table is Z_INDEX and that has a specific meaning when talking about layering of graphics.
Yes there is a bug but there is a simple workaround, please try :
displayLayerRepository.findAll(Sort.by(Sort.Order.desc("zIndex").ignoreCase()));
I suppose DisplayLayerRepository extends JpaRepository.
It's similar when you use parameters in your findBy method. Define in your repository empty method :
public List<DisplayLayer> findByDisplayContainerId(Long pDisplayLayerId, Sort sort);
Then you can call it by :
displayLayerRepository.findByDisplayContainerId(1L, Sort.by(Sort.Order.desc("zIndex").ignoreCase()));
You can use JpaSort in query. As in your case, it looks something like this:
Sort sort = JpaSort.unsafe(Sort.Direction.ASC, "zIndex");
List<DisplayLayer> zIndexes = displayLayerRepository.findByZIndex(pZIndex, sort);
and your query in Repository will look like this:
public List<DisplayLayer> findByZIndex(Long pZIndex, Sort sort);
I am using Spring Data's Rest Repositories from spring-boot-starter-data-rest, with Couchbase being used as the underlining DBMS.
My Pojo for the object is setup as so.
#Document
public class Item{
#Id #GeneratedValue(strategy = UNIQUE)
private String id;
#NotNull
private String name;
//other items and getters and setters here
}
And say the Item has an id of "xxx-xxx-xxx-xxx" and name of "testItem".
Problem is, that when I want to access the item, I need to be accessible by /items/testItem, but instead it is accessible by /items/xxx-xxx-xxx-xxx.
How do I get use its name instead of its generated id, to get the data.
I found out the answer to my own question.
I just need to override the config for the EntityLookup.
#Component
public class SpringDataRestCustomization extends RepositoryRestConfigurerAdapter {
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.withEntityLookup().forRepository(UserRepository.class).
withIdMapping(User::getUsername).
withLookup(UserRepository::findByUsername);
}
}
Found the info here, though the method name changed slightly.
https://github.com/spring-projects/spring-data-examples/tree/master/rest/uri-customization
If you want query the item by name and want it perform as querying by id,you should make sure the name is unique too.You cant identify a explicit object by name if all objects have a same name,right?
With jpa you could do it like:
#NotNull
#Column(name="name",nullable=false,unique=true)
private String name;
I have the following two resources, and their association;
#Table(name = "Item")
#Data
#Entity
public class Item {
#ManyToOne
#JoinColumn(name = "fk_wrapper")
private Wrapper wrapper;
#Id
#GeneratedValue
private String id;
private Integer someValue;
}
and;
#Table(name = "Wrapper")
#Data
#Entity
public class Wrapper {
#Id
#GeneratedValue
private String id;
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "fk_wrapper")
private List<Item> items;
private String someField;
}
Then, first, I create a Wrapper;
POST http://localhost:8080/wrappers/
{
"someField": "asd"
}
http://localhost:8080/wrappers/1 created, then I create two Item's, linked to this Wrapper;
POST http://localhost:8080/items/
{
"someValue": "5",
"wrapper": "http://localhost:8080/wrappers/1"
}
&
POST http://localhost:8080/items/
{
"someValue": "7",
"wrapper": "http://localhost:8080/wrappers/1"
}
After all this, when I call the endpoint http://localhost:8080/wrappers/1/items, I get the list of these two items, as expected, but what the trouble is that, I cannot seem to have a sorting feature on this endpoint. I seem to be able to sort in http://localhost:8080/items endpoint, but while fetching with association, there doesn't seem to be a sorting feature. Is this lack of sorting is intended, or am I lacking some configuration?
P.S. when I create a custom search method, for example;
#RepositoryRestResource
public interface ItemRepository extends JpaRepository<Item, String> {
List<Item> findByWrapper_Id(#Param("id") String id, Sort sort);
}
Then I can use the sorting with http://localhost:8080/items/search/findByWrapper_Id endpoint, but too ugly imo, considering there is already an auto-generated endpoint.
Spring Data Rest doesn't support sorting on the associations.
You seem to have already found the best way to do what you need, according to the Spring Data Rest team - create a query for fetching the data you need. That will indeed support both pagination and sorting.
The reason why it's not supported has to do with the time when the queries are made to fetch the main resource (before the association endpoints are built) and the facts that the association endpoint makes use of the the loaded entity associations directly and that for supporting sort, a new query would need to be made anyway.
More detailed information can be found here:
https://jira.spring.io/browse/DATAREST-725?focusedCommentId=122244&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-122244
Cheers!
I am trying to utilize a NamedEntityGraph to limit the return data for specific queries. Mainly I do not want to return full object details when listing the object. A very simple class example is below.
#Entity
#Table(name="playerreport",schema="dbo")
#NamedEntityGraphs({
#NamedEntityGraph(name = "report.simple",
attributeNodes =
{#NamedAttributeNode(value="intId")
}
)
})
public class PlayerReportEntity {
#Id
#Column(name="intid",columnDefinition="uniqueidentifier")
private String intId;
#Column(name="plyid",columnDefinition="uniqueidentifier")
#Basic(fetch=FetchType.LAZY)
private String plyId;
#ManyToOne(fetch=FetchType.LAZY)
#JoinColumn(name = "plyid", insertable=false,updatable=false)
private PlayerEntity player;
No matter what I do to plyId and player are always returned. Is there any way to only return the requested columns (intId) ?
As for the collection Hibernate does not do the join for the player object but it still returns player as null. So that part is working to an extent.
I am using a JPARepository below to generate Crud Statements for me
public interface PlayerReportRepository extends JpaRepository<PlayerReportEntity, String> {
#EntityGraph(value="report.simple")
List<PlayerIntelEntity> findByPlyId(#Param(value = "playerId") String playerId);
#Override
#EntityGraph(value="report.simple")
public PlayerIntelEntity findOne(String id);
}
A chunk of text from here - "Hence it seems that the #NamedEntityGraph only affects fields that are Collections, but fields that are not a Collection are always loaded." from JIRA
Please use the Example 47 on this page and use repositories accordingly.
In essence, hibernate is right now loading all the feilds in the class and for collections it will work if you follow the example stated above.
Thanks.