I use Spring Data REST with JPA. I have a User entity that has a many to one relationship with another called AccountStatus modeled in a separate RDBMS table. The JSON representation looks like this:
{
"id": "123"
"username": "user1",
"accountStatus": {
"id": "1",
"status": "Active"
}
}
The relationship in the User entity is:
#ManyToOne(optional = false)
#JoinColumn(name = "account_state")
#Getter #Setter private AccountState accountState;
Now I try to change the account status using a PATCH request on /users/123 and the payload:
{"accountState":{"id":0}}
But I get an error:
"identifier of an instance of com.domain.account.AccountState was
altered from 1 to 0; nested exception is org.hibernate.HibernateException:
identifier of an instance of com.domain.account.AccountState was
altered from 1 to 0"
I also tried to use #HandleBeforeSave/#HandleBeforeLinkSave to fetch the new AccountState from the repository and replace user.accountStatus with no success.
What am I doing wrong?
It really depends if you have an exported repository for AccountState. If you do you can update your account state with a PATCH against /users/{id}:
{
"accountState": "http://localhost:8080/accountStates/2"
}
So you are using the URI of your account state to reference the resource to assign
Related
I am a newbie to Java Persistence API and Hibernate and using Spring JPA repositories for querying in DB. Now I have two entities in Parent <-> Child relationship with Parent entity with #OneToMany and Child entity with #ManyToOne mapping.
Parent Entity:-
#Entity
#Table(name = "PERSONS")
public class Persons {
...
#OneToMany(mappedBy = "person", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
public List<Cards> cards = new ArrayList<Cards>();
...
}
Child Entity:-
#Entity
#Table(name = "CARDS")
public class Cards {
...
#ToString.Exclude
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "PERSON_ID", nullable = false, insertable = false, updatable = false)
public Person person;
...
}
And I am using my PersonsRepository like below :-
#Repository
public interface PersonsRepository extends JpaRepository<Persons, String> {
....
}
Now the fetchType being used in the relationship is LAZY at both the ends. Now whenever I tried to loop over a List and tried to process the cards for each using person.getCards(), it gives me below error:-
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.xxx.abc.Persons.cards, could not initialize proxy - no Session
at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:606)
at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:218)
at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:585)
at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:149)
at org.hibernate.collection.internal.PersistentSet.iterator(PersistentSet.java:188)
at java.util.Spliterators$IteratorSpliterator.estimateSize(Spliterators.java:1821)
at java.util.Spliterator.getExactSizeIfKnown(Spliterator.java:408)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
Now I have found everyone saying that using LAZY is the best approach in Hibernate and it says lot more about the correct design of code as well. I agree the way I have used person.getCards() will not have any open session and that is the reason it is giving me LazyInitializationException but the intent behind this is to save a lot more DB calls.
Assuming I have 1000 persons list, that means I have to make 1000 separate calls to getCards() for each person. That's why if I use the FETCHTYPE.EAGER in Person #OneToMany, what is the performance impact since everything will be fetched eagerly.
Need suggestions about the best practices followed for such kind of problems. TIA.
Edit:-
I have a method in service class where I am using #transactional for that like below:-
#Transactional(readOnly = true)
public void fetchData(Integer param1, Timestamp param2, Timestamp param3, List<String> param4, NavigableMap<Long, List<Cards>> param5) {
List<Persons> validPersons = personRepo.getCardsPerPerson(param2, param3);
if(validPersons != null && !validPersons.isEmpty()) {
// store the cards on the basis of epoch timestamp
prepareTimestampVsCardsMap(validPersons, param4, param5);
}
}
private void prepareTimestampVsCardsMap(List<Persons> validPersons, List<String> uList, NavigableMap<Long, List<Cards>> timestampVsCardsList) {
for(Person person : validPersons) {
Long epoch = order.getOrderTime().getTime();
Set<Cards> cardsPerPerson = person.getCards();
}
}
Also, the query being used in repository for getting the cards associated to a person is using join fetch as below:-
#Query(value = "select p from Person p join fetch Cards c on p.id = c.id WHERE p.orderTime BETWEEN ?1 AND ?2 ORDER BY orderTime ASC")
public List<Person> getCardsPerPerson(Timestamp param1, Timestamp param2);
I am still getting the same above mentioned LazyInitializationException. Can anyone please help.
First of all, it's always better to use FetchType.LAZY instead of FetchType.EAGER. Why? Because you might not need all the data every time. If you want to return a list of Persons and display them somehow, somewhere, do you need to fetch all of their cards as well? If not, then FetchType.LAZY would be the better option, and you would then control how much data you need.
LazyInitializationException usually indicates that you didn't fetch all the data you need while your Session was opened. There are many ways to fetch associated data (none of which is keeping the Session opened while processing request):
1. using join fetch in your JPQL/HQL
#Query("select p from Person p join fetch p.cards where ...")
List<Person> getCardsPerPerson(Timestamp param1, Timestamp param2);
2. if you're using Spring Data, you could use #EntityGraph instead of join fetch
#EntityGraph(attributePaths = { "cards" })
List<Person> getPersons();
That way, every time you call getPersons, it will fetch cards as well. Of course, you couldn't use this one if you have to write #Query.
If you're using Spring Data's naming conventions for some simple queries, then #EntityGraph would be an option for fetching associations.
3. using Criteria API
Again, if you're using Spring Data, this is just a fallback solution in case you end up with MultipleBagFetchException. I will not go into details for this one, but in case you encounter this exception, you'll find solution in Vlad Mihalcea's blog post The best way to fix the Hibernate MultipleBagFetchException.
You are under the misconception that EAGER loading means Hibernate will fetch all data with one statement, this is false. With EAGER as a strategy, the framework will just do every query required to fetch all data for every entity.
Example: If one entity has 2 EAGER relationships, fetching one will result in 3 statements, one to load the entity, one for each of its relationships. If you have 3 entities, you will have 7 statements, the initial statement loading the 3 objects, plus 2 per object.
When your treatment requires everything, there is no real performance impact at the moment. But most applications are not made of one treatment. This means every treatment in your application will load everything which is EAGER, even if not needed. This will effectively slow everything down. You also risk loading all your database in memory if everything is in EAGER.
This is why LAZY is the recommended approach.
As for your LazyInitializationException, it seems in your stack trace that you are using the stream API. It's a wild guess due to missing details, but JPA/Hibernate doesn't handle sharing a session between threads, so if you are using parrallelStream it could cause the problem.
I'm new to thymeleaf, and have a little problem causing headache.
I have an entity "Payment", and Payment has "Manager" entity as attribute.
#ManyToOne
#JoinColumn(name = "manager")
private Manager manager;
And Manager Entity has attributes like id, name, .. so on.
I want to access to the 'name' attribute from Payment Dto, like below.
<td><span th:text="${payment.manager.id}"></span></td>
However, it generates error code.
org.springframework.expression.spel.SpelEvaluationException: EL1007E: Property or field 'name' cannot be found on null
I tried <td><span th:text="${payment.manager}"></span></td> this, and the program was running without error, although nothing was shown in that line.
How can I access to inner attribute(ex. name) of a nested objects like
Payment { Manager { id, name, ... } }
this?
First try to run the API on postman and check whether the data is coming right means
payment :{
id: "",
.
.
"manager":{
"name":"",
.
.
.
}
}
If not then there is problem while saving the entity payment. You saved the payment correctly but may be you didn't set the entity manager in the payment entity using ManyToOne relationship. Like this
payment.setManager(manager);
Maybe you did an error in saving that.
I have a manytoone relationship between two entities and i want to know if there's a better option to save using an existing id, for example, in the example below, should i send a company id inside json or first create a Role and the using PUT update the Role with company Id. Or maybe inside the Controller find the company entity and then set in the new Role entity and after that, save it. How to proceed in this case?
#Entity
data class Role(
val name: String = "",
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "company_id")
val company: Company,
val type: RoleType,
val description: String = ""
)
If companyfield of Role is mandatory (Not Null) then you need to create first Company so that you can link with ID or Name or whatever as foreign key. Then you first create a Company and later a Role related to it.
{
"name" : "Role Name - 1",
"company" : 1, // or "Company-"
"type" : "Type - 1",
"description" : "This a test description"
}
OK so let's start self referencing object, something like this:
#Data
#Entity
public class FamilyNode {
#Id
#GeneratedValue
private long id;
private boolean orphan;
#ManyToOne
private FamilyNode parent;
}
And a standard repository rest resource like this:
#RepositoryRestResource(collectionResourceRel = "familynodes", path = "familynodes")
public interface FamilyNodeRepository extends CrudRepository<FamilyNode, Long> {
}
Now, let's assume some parent objects which I want to link with already exist with ID=1 and ID=2, each of which were created with a POST to /api/familynodes which looked like this:
{
"orphan": true,
}
If I attempt to create a new client (ID=3) with something like this using a POST request to /api/familynodes, it will work fine with the linked resource updating fine in the DB:
{
"orphan": false,
"parent": "/api/familynodes/1"
}
However, if I attempt to do a PUT with the following body to /api/familynodes/3, the parent property seems to silently do nothing and the database is not updated to reflect the new association:
{
"orphan": false,
"parent": "/api/familynodes/2"
}
Similarly (and this is the use case that I'm getting at), a PUT like this will only update the orphan property but will leave the parent untouched:
{
"orphan": true,
"parent": null
}
So you now have a record which claims to be an orphan, but still has a parent. Of course you could do subsequent REST requests to the resource URI directly but I'm trying to make rest operations atomic so that it's impossible for any single rest query to create invalid state. So now I'm struggling with how do that with what seems like a simple use case without getting into writing my own controller to handle it - am I missing a mechanism here within the realm of spring data rest?
This is the expected behaviour for PUT requests in Spring Data Rest 2.5.7 and above wherein a PUT request does not update the resource links, only the main attributes.
As detailed here by Oliver Gierke:
If we consider URIs for association fields in the payload to update those associations, the question comes up about what's supposed to happen if no URI is specified. With the current behavior, linked associations are simply not a part of the payload as they only reside in the _links block. We have two options in this scenario: wiping the associations that are not handed, which breaks the "PUT what you GET" approach. Only wiping the ones that are supplied using null would sort of blur the "you PUT the entire state of the resource".
You may use a PATCH instead of PUT to achieve the desired result in your case
Does Spring Data treats mongo nested "id" attributes differently? I explain my problem: I have collection matches with the following structure
"teams": [
{
"id" : "5601",
"name" : "FC Basel"
},
... // more
]
When I want to retrieve all the matches which has team id 5601 I execute the following query
db.matches.find({ "teams.id" : "5601"})
Which works perfectly and returns some objects.
When I make a method
public List<MatchMongo> findByTeams_id(String id);
on my MatchRepository interface I get 0 results back while there are.
Logs shows
Created query Query: { "teams.id" : "5601"}, Fields: null, Sort: null
find using query: { "teams.id" : "5601"} fields: null for class: class
MatchMongo in collection: matches
So the query he makes seems to be the right one... :S
Trying with other fields (referee.name for ex.) works.
I even tried with the #Query annotation, but can't get it to work
Is there another solution? Is this a bug or am I doing something wrong?
Oh found the solution:
MatchMongo had List<TeamMongo> teams; on whereI had
#Id
private String id;
#Field(value = "id")
private String teamIdAttr;
So the method should be called
public List<MatchMongo> findByTeams_teamIdAttr(String id);
Never thought the method name should reflect objects attributes instead of collection structure
Thanks #martin-baumgartner your comment helped to solve this :)