Spring data rest add element to collection [duplicate] - spring

This question already has answers here:
How to maintain bi-directional relationships with Spring Data REST and JPA?
(4 answers)
Closed 6 years ago.
I'm trying to add an element to a #OneToMany relation following this tutorial on Spring Data Rest, I'm using the request below to add an account to a user accounts collection.
curl -v -X PUT -H "Content-Type: text/uri-list" -d "http://localhost:8080/account/1" http://localhost:8080/user/1/accounts
The request respond with a 204 and nothing changes when I check the user accounts list. Debugging the app I can see that the entity is being saved, so maybe a transaction commit is not happening. I also noticed that if I change the relation to a #OneToOne, the request works as expected.
User class
#Entity
#Getter
#Setter
#Builder
#NoArgsConstructor
#AllArgsConstructor
#ToString(exclude = "accounts")
#EqualsAndHashCode(of = "id", callSuper = false)
#SequenceGenerator(name = "user_sq", sequenceName = "user_sq")
public final class User extends BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.AUTO, generator = "user_sq")
private Long id;
//other properties
#OneToMany(cascade = CascadeType.ALL, mappedBy = "owner", orphanRemoval = true)
private List<Account> accounts;
// #OneToOne(cascade = CascadeType.ALL)
// private Account accounts;
}
Account class
#Entity
#Getter
#Setter
#Builder
#NoArgsConstructor
#AllArgsConstructor
#EqualsAndHashCode(of = "id", callSuper = false)
#SequenceGenerator(name = "account_sq", sequenceName = "account_sq")
public final class Account extends BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.AUTO, generator = "account_sq")
private Long id;
//other properties
#ManyToOne
private User owner;
}
Repositories
#RepositoryRestResource(collectionResourceRel = "accounts", path = "account")
public interface AccountRepository extends PagingAndSortingRepository<Account, Long> {}
#RepositoryRestResource(collectionResourceRel = "users", path = "user")
public interface UserRepository extends PagingAndSortingRepository<User, Long> {}

Try to use something like this body in your POSt or PUT/PATCH requests:
{
// user properties...
"accounts": [
"http://localhost:8080/accounts/1",
"http://localhost:8080/accounts/2"
]
}
To create new User (with his accounts) use POST request to http://localhost:8080/users
To update current User (and his accounts) use PUT/PATCH request to http://localhost:8080/users/1 (1 - is the ID of current user)

Related

Why does DataJpaTest fail when saving OneToMany-related data in this pattern?

I have three Hibernate #Entity's below that mimic a failure in my production app:
#Entity
#Data
#SuperBuilder(toBuilder = true)
#EqualsAndHashCode(callSuper = true)
#NoArgsConstructor
#AllArgsConstructor
public class Dog extends Animal {
String barkType;
}
The Dog entity uses JOINED inheritance with this class, Animal:
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
#Data
#SuperBuilder(toBuilder = true)
#NoArgsConstructor
#AllArgsConstructor
public class Animal {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Type(type = "uuid-char")
private UUID id;
#OneToMany(cascade = CascadeType.REMOVE)
#JoinColumn(name = "animalId", referencedColumnName = "id", insertable = false, updatable = false)
#Builder.Default
private List<Toy> toys = new ArrayList<>();
}
This Toy Entity is related to the parent class, Animal
#Entity
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class Toy {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Type(type = "uuid-char")
private UUID id;
#Type(type = "uuid-char")
private UUID animalId;
private String shape;
}
And here is my implementation I am testing:
#Service
#AllArgsConstructor
public class DogService {
DogRepository repository;
ToyRepository toyRepository;
#Transactional
public Dog saveDogDTO(DogDTO dogDTO) {
Dog entity = Dog.builder()
.barkType(dogDTO.getBarkType())
.build();
repository.save(entity);
toyRepository.save(Toy.builder()
.shape(dogDTO.getToyShape())
.animalId(entity.getId())
.build());
return entity;
}
}
Here is my failing Test, which fails on the LAST line:
#DataJpaTest
class DogServiceTests {
private DogService dogService;
#Autowired
private DogRepository dogRepository;
#Autowired
private ToyRepository toyRepository;
#Test
void save_not_working_example() {
dogService = new DogService(dogRepository, toyRepository);
var dogDTO = DogDTO.builder()
.barkType("big bark")
.toyShape("some shape")
.build();
var savedDog = dogService.saveDogDTO(dogDTO);
assertThat(dogRepository.count()).isEqualTo(1);
assertThat(toyRepository.count()).isEqualTo(1);
var findByIdResult = dogRepository.findById(savedDog.getId());
assertThat(findByIdResult.get().getToys()).hasSize(1);
}
}
The test failure message:
Expected size: 1 but was: 0 in:
[]
java.lang.AssertionError:
Expected size: 1 but was: 0 in:
[]
The issue seems to be that the double JPA repository save clashes within the #Transaction. Is there a way to overcome this issue? I tried adding #Transactional(propagation = Propagation.NEVER) to the test, but then I get this failure:
failed to lazily initialize a collection of role: com.example.datajpatest.demo.models.Animal.toys, could not initialize proxy - no Session
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.datajpatest.demo.models.Animal.toys, could not initialize proxy - no Session
#DataJpaTest is annotated #Transactional so your test method is all wrapped in a single transaction, and hence a single EntityManager. You could make your test pass by calling EntityManager.detach() on the savedDog before querying using findById(). You could also fix it by manually setting up the dog's toys in the DogService. That would be my recommendation because otherwise sooner or later you might find the same inconsistency bug in production code - the transaction boundaries just have to shift a bit and that would be quite hard to spot. In a way #DataJpaTest has done you a favour by pointing out the problem, albeit somewhat indirectly.
Ultimately, the database state doesn't match the state of the EntityManager cache, so you have to clear the cache to get the result you want. Starting a new transaction would clear the cache too, and that's what is probably happening in production. Hibernate trusts you to make the object graph match the database state when you save (or flush). If they don't match then Hibernate has no way of knowing without querying the database, which it would regard as redundant and inefficient.
Try this mapping here instead:
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
#Data
#SuperBuilder(toBuilder = true)
#NoArgsConstructor
#AllArgsConstructor
public class Animal {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Type(type = "uuid-char")
private UUID id;
#OneToMany(mappedBy = "animal", cascade = CascadeType.REMOVE)
#Builder.Default
private List<Toy> toys = new ArrayList<>();
}
#Entity
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class Toy {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Type(type = "uuid-char")
private UUID id;
#ManyToOne(fetch = LAZY)
#JoinColumn(name = "animalId")
private Animal animal;
private String shape;
}

LazyInitializationException when get EAGER fetch object OneToOne

I have two entity
#Entity
#Table(name = "user")
#Data
#Builder
#EqualsAndHashCode(callSuper=false)
#ToString
#AllArgsConstructor
#NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "name")
private String name;
#OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#PrimaryKeyJoinColumn
private UserLastLogin userLastLogin;
}
#Entity
#Table(name = "lastLogin")
#EqualsAndHashCode(onlyExplicitlyIncluded = true)
#ToString(onlyExplicitlyIncluded = true)
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
public class UserLastLogin implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "name")
private String userName;
#Column(name = "date")
private LocalDateTime date;
#OneToOne
#MapsId
#JoinColumn(name = "name")
private User user;
}
I use spring boot with spring data and jpa, hibernate in latest version.
In documentation is that #OneToOne is default EAGER, but when i get eager fetch object, i get lazyInitializationException when i not use #Transactional in get method. I don't understant why...
public UserDto getUser(String userName) {
var user= userRepository.getById(userName);
d.getSystemUserLastLogin(); // this throw lazy initialization exception
return mapper.entityToDto(d);
}
When i'will mark this method #Transactioal, this work. But, not recommendend used transactions in get method. I need use EAGER fetch in this relationship.
When i view query hibernate, i have one select, but children object is not available.
Hibernate:
select
user0_.name as nazwa1_4_0_,
user2_.name as name1_23_2_,
user2_.data as data3_23_2_
from
user0_
left outer join
last_login user2_
on user0_.name=user2_.name
where
user0_.name=?
The problem was that despite the fetch eager, lazy was used. This was due to the use of the getById method from the repository, which retrieves only the object's references and snaps all the fields when lazy is retrieved. Changing to findById solves the problem as findById takes an object, not a reference.
I would recommend you to use secondary tables instead like this:
#Entity
#Table(name = "user")
#Data
#Builder
#EqualsAndHashCode(callSuper=false)
#ToString
#AllArgsConstructor
#NoArgsConstructor
#SecondaryTable(name = "lastLogin", pkJoinColumns = #PrimaryKeyJoinColumn(name = "name"))
public class User implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "name")
private String name;
#Column(table = "lastLogin", name = "date")
private LocalDateTime date;
}
Also see https://www.baeldung.com/jpa-mapping-single-entity-to-multiple-tables for more details.

Springboot add problem in oneTOMany relation

I'm writing 3 tables in the following relation:
Club class:
#Setter
#Getter
#Entity
#Table(name = "Club")
public class Club {
#Id
#GeneratedValue
private Long id;
private String name;
private String type;
private String mainPage;
private String logo;
#OneToMany(mappedBy="clubProductKey.club", cascade = CascadeType.ALL)
#JsonIgnoreProperties(value = "clubProductKey.club", allowSetters=true)
private Set<ClubProduct> clubProducts;
...
Product class:
#Setter
#Getter
#Entity
#Table(name = "Product")
public class Product {
#Id
#GeneratedValue
private Long id;
#OneToMany(mappedBy="clubProductKey.product", cascade = CascadeType.ALL)
#JsonIgnoreProperties(value = "clubProductKey.product", allowSetters=true)
private Set<ClubProduct> clubProducts;
...
ClubProduct class:
#Setter
#Getter
#Entity
#Table(name = "ClubProduct")
public class ClubProduct {
#EmbeddedId
private ClubProductKey clubProductKey;
...
ClubProductKey class:
#Setter
#Getter
#Embeddable
public class ClubProductKey implements Serializable {
#ManyToOne(cascade = {CascadeType.MERGE,CascadeType.REFRESH })
#JoinColumn(name = "club_id", referencedColumnName = "id")
#JsonIgnoreProperties(value = "clubProducts", allowSetters=true)
private Club club;
#ManyToOne(cascade = {CascadeType.MERGE,CascadeType.REFRESH })
#JoinColumn(name = "product_id", referencedColumnName = "id")
#JsonIgnoreProperties(value = "clubProducts", allowSetters=true)
private Product product;
...
ClubProductRepository class:
public interface ClubProductRepository extends JpaRepository<ClubProduct, ClubProductKey> {
public List<ClubProduct> findByClubProductKeyClub(Club club);
public List<ClubProduct> findByClubProductKeyProduct(Product product);
}
I try to save clubProduct like this:
#Service
public class ClubProductServiceImp implements ClubProductService {
#Autowired
private ClubProductRepository clubProductRepository;
...
ClubProduct savedClubProduct = clubProductRepository.save(clubProduct);
return savedClubProduct;
}
However I find that the clubProduct is not saved in the clubProducts list in the club or product entity, the list is null. Must I add lines like club.getClubProducts.add(clubProduct) or is there any other way to make it added automatically?
Thank you.
The #OnetoMany mapping in your Club class uses the attribute mappedby which means that it represents the owning side of the relation responsible for handling the mapping. However, we still need to have both sides in sync as otherwise, we break the Domain Model relationship consistency, and the entity state transitions are not guaranteed to work unless both sides are properly synchronized.
The answer is yes, you have to manage the java relations yourself so that the clubProducts gets persisted. You are using an instance of the repository class club to persist the data so , you should add a setter method like :
public void addClubProduct(ClubProduct clubProduct) {
if (clubProduct!= null) {
if (clubProduct== null) {
clubProduct= new ArrayList<ClubProduct>();
}
clubProducts.add(clubProduct);
clubProduct.setClubProduct(this);
}
}
also a method to remove it from the list and use these method in your code to set the values to the list properly before initiating save . Read related article

Spring Data REST javax.persistence.RollbackException: Error while committing the transaction

I have tow Entities:
#Data
#Entity(name = "users")
#NoArgsConstructor
#RequiredArgsConstructor
#EqualsAndHashCode
public User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name="id", unique = true, nullable = false)
private Integer userId;
private String userName;
#OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
#JoinColumn(name = "user_id")
#Fetch(value = FetchMode.SUBSELECT)
private List<Degree> degreeList;
}
and
#Data
#Entity(name = "degrees")
#NoArgsConstructor
#RequiredArgsConstructor
#EqualsAndHashCode
public Degree {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name="id", unique = true, nullable = false)
private Integer degreeId;
private String degreeTitle;
}
RestResources are:
#RepositoryRestResource(collectionResourceRel = "users", path = "users")
public interface UserRestRepository extends CrudRepository<User, Integer> {
}
and then:
#RepositoryRestResource(collectionResourceRel = "degrees", path = "degrees")
public interface DegreeRestRepository extends CrudRepository<Degree, Integer> {
}
I have to add new bar in barList of foo using https://www.myhost.com/api/users/{userId}/degreeList endpoint as shown in image:
but I am getting:
{
"timestamp": "2020-04-17T05:16:51.520+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Error while committing the transaction",
"path": "/api/users/4/degreeList"
}
I have searched and found that I have to save Degree then update degreeList, how can I do that please guide
Spring Data REST is URI-based (following the HATEOAS rules). If you want to add "degree" to "degreeList", indeed you first need to create the degree:
HTTP POST to https://www.myhost.com/api/degrees
The request body has to be of content type application/json
After that, you get back the Degree ID in the response. You use this ID to add the Degree to the list:
HTTP POST to https://www.myhost.com/api/users/{userId}/degreeList
Important: this time the request content type is text/uri-list! The body must be plain text and should just contain the URI
https://www.myhost.com/api/degrees/{degreeId}

findBy not working with inherited properties

I have the following model and repository:
#Entity
#Table(name = "db_user", uniqueConstraints = { #UniqueConstraint(columnNames = "email") })
public class User {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_user")
#SequenceGenerator(name = "seq_user", sequenceName = "seq_user")
#Column(name = "id")
private Long id;
// ...
}
#Entity
#Table(name = "movie")
public class Movie extends AbstractItem {
// Id column inherited from AbstractItem
// ...
}
#Entity
#Table(name = "movie_user")
public class MovieOwnership extends AbstractOwnership {
#ManyToOne
private Movie movie;
// ...
}
#MappedSuperclass
public abstract class AbstractOwnership{
#Id
#SequenceGenerator(name = "seq_default", sequenceName = "seq_default")
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_default")
#Column(name = "id")
private Long id;
#ManyToOne
private User owner;
// ...
}
public interface MovieOwnershipRepository extends QueryDslJpaRepository<MovieOwnership, Long> {
List<MovieOwnership> findByOwnerId(Long ownerId);
MovieOwnership findByOwnerIdAndMovie(Long ownerId, Movie movieId);
List<MovieOwnership> findByOwnerIdAndMovieIdIn(Long ownerId, Set<Long> movieIds);
}
I'm trying to use Spring's findBy requests to fetch MovieOwnerships by owner or movie, using the id field of both entities. I'm able to work directly with the owner's id, but using MovieId in my requests seems broken (I can use the whole Movie object though). In the code above, the first two findBy are fine but the last one throws this exception:
Caused by: java.lang.IllegalArgumentException: Unable to locate
Attribute with the the given name [movieId] on this ManagedType
[carrm.app.data.AbstractOwnership]
It compiles if I try with another property from Movie (like findByMovieTitle), but I can't make it work on the id.
Any idea how to solve this?
I tried the same with JpaRepository instead of QueryDslJpaRepository.
The SQL is generated correctly:
select movieowner0_.id as id1_1_, movieowner0_.owner_id as owner_id2_1_, movieowner0_.movie_id as movie_id3_1_
from movie_ownership movieowner0_
left outer join user user1_ on movieowner0_.owner_id=user1_.id
left outer join movie movie2_ on movieowner0_.movie_id=movie2_.id
where user1_.id=? and (movie2_.id in (?))
So it must be a QueryDslJpaRepository implementation bug.
I would suggest you use JpaRepository instead.

Resources