Default qualifier/filter for Spring Data Rest with JPA - spring

I'm trying to create a default filter in Spring Data Rest with JPA. I have a Reward class, and the Reward can have an exclusive relationship.
Here is a shortened version of the Reward class
#Data
#Entity
public class Reward implements Identifiable<UUID> {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private UUID id;
#ManyToOne(fetch = FetchType.LAZY)
private Owner exclusiveOwner;
}
The Owner class has a #ManyToOne relationship to Reward.
I'd like to have it setup so that when /api/rewards is called it only returns Rewards who's exclusive relationship is null (i.e., generally available rewards), and when /api/rewards?exclusiveOnwer=<UUID> it only returns the rewards exclusive to that Owner.
I've gotten the second part to work with QueryDSL by having the RewardRepository extend QueryDslPredicateExecutor<Reward>, but I can't figure out how to get the default qualifier to work. Is there any way to do that?
Update
I have tried a workaround by creating a handler method with QueryDSL Predicates based on the Spring Blog. It is in my RewardsController class, which is a #RepositoryRestController. The method signature is
#GetMapping(path = "/rewards")
public ResponseEntity<?> findAll(#QuerydslPredicate(root = Reward.class) Predicate predicate, final FindParams findParams, final Pageable p, final PersistentEntityResourceAssembler entityAssembler) {
but that gives me the error
org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.querydsl.core.types.Predicate]: Specified class is an interface whenever I hit the endpoint. (FindParams is a simple POJO for capturing parameters. The same error occurs if I use a Map.)
There is a bug with a workaround, but that requires changing the Controller from a #RepositoryRestController to a #RestController, which I don't want to do as 1) my API has a base path of /api, and 2) I already have a method in there that I don't want to break. If I change the Controller to #BasePathAwareController the problem persists.

Related

CRUDRepository findBy foreign key id causing exception: Unable to locate Attribute with the the given name [classroomId] on this ManagedType

I am getting an exception when creating a custom findBy method by a foreign key.
Entity class:
#Entity
#Getter
#Setter
public class Thread {
private #Id #GeneratedValue Long id;
private String subject;
#ManyToOne(fetch = FetchType.LAZY)
#JsonIgnore
private Classroom classroom;
protected Thread() {}
public Long getClassroomId() {
return this.classroom.getId();
}
}
ThreadRepository class:
public interface ThreadRepository extends CrudRepository<Thread, Long> {
List<Thread> findByClassroomId(Long id);
}
I get the exception:
java.lang.IllegalArgumentException: Unable to locate Attribute with the the given name [classroomId] on this ManagedType [com.futurerprood.unicycleservice.entity.threads.Thread]
But the exception goes away if I remove the getClassroomId() in the Thread class. I have this function so that the json serialization will pick up only the classroom id instead of the whole classroom object in an endpoint response.
Why is this function causing the foreign key unable to be found?
You can do one of the following:
Provide the query to the repository method
#Query("select e from Thread t join t.classroom c where c.id = :id")
List<Thread> findByClassroomId(Long id);
Rename the repository method
List<Event> findByClassroom_Id(Long id);
Update
Explanation as to why these two are working
First, have a look at https://docs.spring.io/spring-data/jpa/docs/1.4.3.RELEASE/reference/htmlsingle/#d0e391 and understand how property traversal based on method name happens in Spring data JPA in order to generate the query and how ambiguity resolution is recommended.
In the first one, we tell spring data, it does not need to do property traversal to generate the JPA query as we are giving the query so it does not get any ambiguity.
In the second, as recommended in the reference, we are resolving the ambiguity for Spring Data JPA by telling it to go to Classroom object first. But as #crizzis pointed out under the question comment, Spring data should have treated it as ambiguity in the first place

Unable to initialize lazy-loaded relationship inside of `#Transactional` method

I have a set of simple models like this (getters and setters omitted for brevity):
#Entity
public class Customer {
#Id
private Integer id;
}
#Entity
public class Order {
#Id
private Integer id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "customer_id")
private Customer customer;
}
I am trying to load an Order using a Spring JPA repository with a findById method, including the customer.
First I tried this:
#Transactional
Optional<Order> findById(Integer id) {
return repository.findById(id);
}
But when I tried to access Customer I got a LazyInitializationException: could not initialize proxy - no Session. So after referring to some other questions, I updated my method to be a bit uglier, but to explicitly call Hibernate.initialize:
#Transactional
Optional<Order> findById(Integer id) {
return repository.findById(id)
.map( order -> {
Hibernate.initialize(order.getCustomer());
return order;
);
}
But I still get org.hibernate.LazyInitializationException: could not initialize proxy - no Session. repository is a regular CrudRepository which provides the findById method out-of-the-box.
How can I initialize this lazily loaded child entity? My understanding is that the #Transactional indicates that I should still be within the transaction for the entirety of this method call. The only thing further downstream is the repository itself, which is just an interface, so I'm not sure how else to go about forcing the load of this child entity.
The Order entity and everything else in it is retrieved properly from the database; it's only when I try to get the lazy-loaded child entities that we start having issues.
The only way I managed to get this working was to write a custom hql method in the Repository using a left join fetch. While that works, it clutters up my repository with a method that is pretty much a duplicate of another and which I'm pretty sure I'm not actually supposed to need (so I would rather not do it this way.)
Spring-Boot 2.1.4.RELEASE, Spring 5.1.6.RELEASE, Hibernate 5.3.7.Final.
You have to define the method as public. See "Method visibility and #Transactional" in the spring docs.
This should work:
#Transactional
public Optional<Order> findById(Integer id) {
Optional<Order> order = repository.findById(id);
order.ifPresent(o -> Hibernate.initialize(o.getCustomer()));
return order;
}

Spring data #ReadOnlyProperty causing unexpected behavior

I have a Model attribute that needs to set #ReadOnlyProperty so that it won't persist after first inserting the line.
Assume my model like below
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(updatable = false, nullable = false)
#JsonIgnore
private Long id;
#Column(unique = true, nullable = false)
#ReadOnlyProperty
private String openId;
}
then I have a UserRepository:
public interface UserRepository extends JpaRepository<User, Long> {
}
then I provide 2 Restful API for POST and PUT.
The create user operation code is as simple as below:
user.setOpenId(1);
userRepository.save(user)
The update user operation is almost the same:
user.setOpenId(2);
user = userRepository.save(user);
I'm surprised that the user's openId attribute will be changed, after POST and then PUT, the returned user object will have the changed value.(user.getOpenId() == 2)
It looks like #ReadOnlyProperty not working, I'm using the RELEASE version of spring-boot-starter-data-jpa. Can someone help explain?
It seems that #ReadOnlyProperty doesn't work. The following bug report is open for years:
Properties with #ReadOnlyProperty annotation are being nullified in PATCH requests
If you want to deny modifying the property via Spring Data Rest endpoints, use the #JsonProperty(access = Access.READ_ONLY) annotation. It affects the JSON deserialization, so the annotated property never reaches Spring Data Rest.
If you also need to deny the writing of the property via Spring Data JPA, you can use the following JPA annotation: #Column(updatable=false) It denies the override on the underlaying JPA level, instead of Spring Data JPA level.

How to use #RestController (Spring) with a child List of object

I'm trying to create a REST service with Spring.
Everything works until I try to add a List of object (CartItem) to my main object (Cart).
This is my main object
#Entity
#Table(name="cart")
public class Cart implements Serializable{
...
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Id
#Column(name="id")
private Integer id;
/*when I add this I get the error. If I remove this, the
REST service works*/
#OneToMany(mappedBy="cart", fetch = FetchType.EAGER)
private List<CartItem> cartItems;
//getter, setter, constructors, other fields ecc.
}
This is the object inside the List:
#Entity
#Table(name="cart_item")
public class CartItem implements Serializable{
...
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Id
#Column(name="id")
private Integer id;
#OneToOne(targetEntity = Product.class, cascade = CascadeType.ALL)
#JoinColumn(referencedColumnName="productId", name="product_id" )
private Product product;
#ManyToOne
#JoinColumn(name="cart_id", nullable=false)
private Cart cart;
//getter, setter, constructors, other fields ecc.
}
This is my controller
#RestController
#RequestMapping(value="rest/cart")
public class CartRestController {
...
#RequestMapping(value = "/", method = RequestMethod.GET)
public List<Cart> readAll() {
return cartService.read();
}
...
}
I get this error:
SEVERE: Servlet.service() for servlet [dispatcher] in context with path
[/webstore] threw exception [Request processing failed; nested exception
is org.springframework.http.converter.HttpMessageNotWritableException:
Could not write JSON: Infinite recursion (StackOverflowError); nested
exception is com.fasterxml.jackson.databind.JsonMappingException:
Infinite recursion (StackOverflowError) (through reference chain:...
I suppose that I had to manage the List inside the Cart object in a particular manner, maybe because i'm using JPA, but I still didn't find a solution on the internet.
Can anyone help me?
This is a serialization recursion problem, it happens because CartItem has a bidirectional mapping back to Cart. So what happens is that
a Cart gets serialized to JSON
all the CartItems inside it get serialized to JSON
the Cart property inside CartItem get serialized to JSON
the CartItems inside the cart get serialized to json, etc. etc.
You will probably want to exclude the CartItem.cart field from serialization by marking it with the #JsonIgnore annotation.
It is only too easy to expose far too much information to the outside world if you use JPA entities directly inside your webservices. Jackson actually has a useful feature called a JsonView which allows you to define which properties get exposed, you can even tailor it per webservice call if you want.
Never ending list? Did you mean a stackOverFlow exception?
If the situation is just like I said,then you should check something like fetch type and the entities' toString() or equal() method or something like that.
For example,there are to entities named A and B and their relationship is one to many(A is the one).If you config both of their fetchType as Eager,then when jpa query A,it will query B too.But B also contains A,so jpa will query A again.This kind of circle loop will cause a stackOverFlow.
By the way, how about providing more info about your problem like the Exception name?It's too hard for me to give you a specific solution,All I can do is to tell you some experiences I have met before.
Well,I created a small project with SpringBoot 2.1.0 and MySql.
It's my cartItem
public class CartItem {
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Id
#Column(name="id")
private Integer id;
#JsonIgnore
#ManyToOne
#JoinColumn(name="cart_id", nullable=false)
private Cart cart;
}
and my cart:
public class Cart {
#GeneratedValue(strategy= GenerationType.IDENTITY)
#Id
#Column(name="id")
private Integer id;
#OneToMany(mappedBy="cart", fetch = FetchType.EAGER)
private List<CartItem> cartItems;
}
Controller is as same as you wrote.After adding a #JsonIgnore to cart filed of CartItem,circle loop is over(before i do that,the program did had a circle loop problem).
Every time you use jpa with #oneToMany,#ManyToOne or #ManyToMany,you should be careful about this problem.This circular reference case could happen when instantiating a object, printing a object or something like this.And of course there is a lot of way to solve it like changing fetch type to LAZY,adding #JsonIgnore,overriding toString() and equal() method.

Spring Data Rest - sort by nested property

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.

Resources