Hibernate #OneToMany relation cascade option not working - spring-boot

I design simple 1:N schema , Account(1):AccountProfileImage(N).
Below codes are entity codes.
// Account.java
#Entity
#Table(name = "account")
#Getter
#NoArgsConstructor(access = AccessLevel.PROTECTED)
#EntityListeners(AuditingEntityListener.class)
public class Account {
#GeneratedValue
#Id
#Column(name = "id")
private Long id;
#Column(name = "email", nullable = false)
private String email;
#Column(name = "password", nullable = false)
private String password;
#Column(name = "first_name", nullable = false)
private String firstName;
#Column(name = "last_name", nullable = false)
private String lastName;
#CreatedDate
#Column(name = "created_at")
private LocalDateTime createdAt;
#OneToMany(mappedBy ="account",cascade = CascadeType.ALL)
private final List<AccountProfileImage> profileImages= new ArrayList<>();
#Builder
public Account(String email,String firstName,String lastName,String password){
this.email=email;
this.firstName=firstName;
this.lastName=lastName;
this.password=password;
}
}
// AccountProfileImage.java
#Entity
#Table(name = "account_profile_image")
#NoArgsConstructor(access = AccessLevel.PROTECTED)
#Getter
#EntityListeners(AuditingEntityListener.class)
public class AccountProfileImage {
#Id
#GeneratedValue
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "account_id")
private Account account;
#Column(name = "image_url")
private String imageURL;
#CreatedDate
#Column(name = "created_at")
private LocalDateTime createdAt;
#Builder
public AccountProfileImage (Account account,String imageURL){
this.account=account;
// this.account.addProfileImage(this);
this.imageURL=imageURL;
}
}
and this is test code for AccountProfileRepository code.
#Test
#Rollback(value = false)
public void saveAccountProfileImageTest() throws Exception {
// given
Account account = Account.builder()
.email("user#email.com")
.firstName("user")
.lastName("user")
.password("1234")
.build();
AccountProfileImage profileImage = AccountProfileImage.builder()
.account(account)
.imageURL("pathToURI")
.build();
AccountProfileImage profileImage2 = AccountProfileImage.builder()
.account(account)
.imageURL("pathToURI2")
.build();
accountRepository.save(account);
// when
List<AccountProfileImage> images = profileImageRepository.findAllByAccount_IdOrderByCreatedAtDesc(1L);
// then
// this assertion fail
assertThat(images.size()).isEqualTo(2);
}
What i want to expect find by List of images whose size is 2 because I add CscadeType.ALL in Account entity class and when creating AccountProfileImage object, I set account member variable in AccountProfileImage object.
this.account=account;
Did I something wrong?
I add below method in Account entity and 2 lines at test code, then it works fine. Do i have to do this everytime? Is there exist another better approach or best practice?
// Account Entity
public void addProfileImages(AccountProfileImage image){
this.profileImages.add(image);
}
// test code
account.addProfileImages(profileImage);
account.addProfileImages(profileImage2);
accountRepository.save(account);
// when
List<AccountProfileImage> images = profileImageRepository.findAllByAccount_IdOrderByCreatedAtDesc(1L);
// then
// this assertion pass
assertThat(images.size()).isEqualTo(2);

In Bi-directional relationships, you have to define the association on both ends of the relationship. To avoid any issues, you can update the helper addProfileImage(..) method to add the AccountProfileImage to the list and set account property of the image to the current account. This is the best practice as this way, the helper method will set up the association across both ends of the bi-directional relationship.
E.g.
public void addProfileImages(AccountProfileImage image){
this.profileImages.add(image); // Add image to profileImages
image.setAccount(this); // Set account property to the current account
}
Test:
#Test
#Rollback(value = false)
public void saveAccountProfileImageTest() throws Exception {
// given
Account account = Account.builder()
.email("user#email.com")
.firstName("user")
.lastName("user")
.password("1234")
.build();
AccountProfileImage profileImage = AccountProfileImage.builder()
.imageURL("pathToURI")
.build();
AccountProfileImage profileImage2 = AccountProfileImage.builder()
.imageURL("pathToURI2")
.build();
// Setup association
account.addProfileImage(profileImage);
account.addProfileImage(profileImage2);
accountRepository.save(account);
// when
List<AccountProfileImage> images = profileImageRepository.findAllByAccount_IdOrderByCreatedAtDesc(1L);
// then
// this assertion fail
assertThat(images.size()).isEqualTo(2);
}

Related

Sending file and JSON in a many-to-many relationship

I have a model called EPI that has a many to many relationship with Model Images, I am not able to do the #PostMapping for this object.
see my code
EPI Entity:
#Entity
#Data
#NoArgsConstructor
#AllArgsConstructor
#Builder
#Table(name = "EPI")
public class EPI implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "Id_EPI")
private UUID id;
#Column(name = "Nome", nullable = false, length = 100)
private String nome;
#Column(name = "Marca", nullable = false, length = 100)
private String marca;
#Column(name = "CA", nullable = false, length = 100)
private String ca;
#Column(name = "Descricao", nullable = false)
private String descricao;
#Column(name = "Foto")
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinTable(name = "epi_images",
joinColumns = {
#JoinColumn(name = "epi_id")
},
inverseJoinColumns = {
#JoinColumn(name = "image_id")
})
private Set<ImageModel> foto;
#Column(name = "Quantidade", nullable = false)
private Integer quantidade;
}
Image Entity:
#Entity
#Data
#NoArgsConstructor
#AllArgsConstructor
#Builder
#Table(name = "image_model")
public class ImageModel implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
private String name;
#Column(name = "type")
private String type;
#Column(name = "image_data", unique = false, nullable = false, length = 100000)
private byte[] imageData;
}
Controller EPI:
#PostMapping("/addNewEPI")
public ResponseEntity<Object> salvarFEPI(#RequestPart("image")MultipartFile file,
#RequestPart("epiModel") EPI epi) throws IOException {
try {
ImageModel foto = productImageService.uploadImage(file);
epi.setFoto((Set<ImageModel>) foto);
return ResponseEntity.status(HttpStatus.CREATED).body(epiService.save(epi));
} catch (Exception e){
System.out.println(e.getMessage());
return null;
}
Service Image:
public ImageModel uploadImage(MultipartFile file) throws IOException {
ImageModel image = new ImageModel();
image.setName(file.getOriginalFilename());
image.setType(file.getContentType());
image.setImageData(ImageUtility.compressImage(file.getBytes()));
return image;
}
As I am passing the parameters in Postman:
enter image description here
Return from Spring Boot:
enter image description here
If anyone can help me I would be very grateful!
I tried passing the parameters in different ways. I just want it to populate my tables passing the parameters of the EPI entity and the Image file.
enter image description here

how to add object with fk to table in jparepository

i had scheme of user parking and detail parking.
user can park many times (one to many)
im trying to add detail parking object to my db, but i dont have idea how to add the fk from the user in the row of the table, its gave me null there.
(ignore from the logic of the model, i just want to understood the logic how can i the object with fk of ther entity)
this is my code:
#PostMapping("/parking")
public String saveCarParking(#ModelAttribute("user") parkingUsers parkingUsers) {
// parkingUsers[id, firstName, lastName, license]
parkingUsers p = new parkingUsers("jhon", "nash", "248651355");
parkingUsersService.saveParkingUser(p);
// parkingDetails[id, entryDate, entryTime, exitDate, exitTime, user_id(FK)]
parkingDetails d = new parkingDetails(LocalDate.now(), null, LocalDate.now(), null);
parkingDetailsService.saveParkingUser(d);
//how i connect parkingDetails object with fk of parkingUsers?
//it adding now row of parkingDetails but without the fk of user
return "redirect:/parkingList";
}
parking user entity:
#Entity
#Table(name ="users")
public class parkingUsers {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private int id;
#Column(name = "first_name")
private String firstName;
#Column(name = "last_name")
private String lastName;
#Column(name = "license")
private String license;
#OneToMany(mappedBy = "parkingUsers", cascade = CascadeType.ALL, orphanRemoval = true)
private List<parkingDetails> parkingDetails = new ArrayList<parkingDetails>();
public parkingUsers() {
}
public parkingUsers(String firstName, String lastName, String license) {
this.firstName = firstName;
this.lastName = lastName;
this.license = license;
}
//setter gettrs and tostring...
entity class of details parking
#Entity
#Table(name ="details")
public class parkingDetails {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private int id;
#Column(name = "entry_date")
private LocalDate entryDate;
#Column(name = "entry_time")
private LocalDateTime entryTime;
#Column(name = "exit_date")
private LocalDate exitDate;
#Column(name = "exit_time")
private LocalDateTime exitTime;
#ManyToOne
#JoinColumn(name="user_id")
private parkingUsers parkingUsers;
public parkingDetails() {}
public parkingDetails(LocalDate entryDate, LocalDateTime entryTime, LocalDate exitDate, LocalDateTime exitTime) {
this.entryDate = entryDate;
this.entryTime = entryTime;
this.exitDate = exitDate;
this.exitTime = exitTime;
}
//test
// public parkingDetails(LocalDate entryDate, LocalDateTime entryTime, LocalDate exitDate, LocalDateTime exitTime, int user_id ) {
// this.entryDate = entryDate;
// this.entryTime = entryTime;
// this.exitDate = exitDate;
// this.exitTime = exitTime;
// this.parkingUsers.setId(user_id);
// }
//setter gettrs and tostring...
In the ParkingDetails entity, you can have a setter for "parkingUsers" variable to set user object.
In your REST api's saveCarParking() method, before calling "parkingDetailsService.saveParkingUser(d);" you can pass the user object to ParkingDetails using setter created in ParkingDetails.
This should work. No need to explicitly extract the user_id from user's object to pass into ParkingDetails.
Adding one more parameter of type ‘parkingUsers‘ in the constructor of ‘ ParkingDetails’ to initialize user in parking class will also work.
(Apart, it is a good practice to start the class name with a capital letter e.g. instead of having class name as parkingDetails, it should be ParkingDetails.)

#GetMapping doesn't display CreditCards under username. #PostMapping doesn't create a new card for user, it only updates it

My User Class looks as follows:
#Entity
#Table(name = "Users")
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "userID")
private Integer userID;
#Column(name = "username",nullable = false, unique = true)
private String username;
#Column(name = "password")
private String password;
#Column(name = "name")
private String name;
#Column(name = "address")
private String address;
#Column(name = "email")
private String email;
#OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<CreditCard> creditCard;
//Constructor, Getters and Setters
CreditCard Class looks :
#Entity
#Table(name = "CreditCards")
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class CreditCard {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "cardID", nullable = false)
private Integer cardID;
#Column(name = "cardName")
private String cardName;
#Column(name = "cardNumber")
private BigInteger cardNumber;
#Column(name = "expirationDate")
private Integer expirationDate;
#Column(name = "securityCode")
private Integer securityCode;
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "user_id", nullable = false)
#JsonIgnore
private User user;
//Constructor, Getters and Setters
CreditCard Resource:
#RestController
#RequestMapping("/geektext/users")
class CreditCardResource {
#Autowired
CreditCardRepository cardsRepository;
#Autowired
UserRepository userRepository;
//Displays CreditCard By Username Search
#GetMapping("/{username}/cards")
public Optional<CreditCard> getCardsByUsername(#PathVariable String username) throws NotFoundException {
if (!userRepository.findByUsername(username).isPresent()){
throw new NotFoundException("User '" + username + "' not found");
}
return cardsRepository.findById(userRepository.findByUsername(username).get().getUserID());
}
//Creates New Card for User
#PostMapping("/{userID}/cards")
public CreditCard loadCard(#PathVariable String userID, #RequestBody CreditCard creditCard) throws NotFoundException {
return userRepository.findByUsername(userID).map(user -> {creditCard.setUser(user);
return cardsRepository.save(creditCard);
}).orElseThrow(() -> new NotFoundException("User '" + userID + "' not found"));
}
}
There is also a UserResource.java , UserRepository (Interface) and CreditCardRepository) but these do not affect the problem I am having. Please how can I fix getting list of cards for User passing username on url. How can user create New/ More than one CreditCard instead of updating the one he has.
You are trying to get a credit-card using your userID
return cardsRepository.findById(userRepository.findByUsername(username).get().getUserID());
Instead, you could search for your credit-card by user. To do this, you should create a method in the credit-card repository interface.
List<CreditCard> findByUser(User user);
Then call this method from your controller
return cardsRepository.findByUser(userRepository.findByUsername(username).get())
The post method has a similar problem. You are trying to get user by username, but passing the userID. Also you set user to your new credit-card, but you don't add a new credit-card to your user. (And change the name of credit-cards variable in the User class to creditCards)
return userRepository.findByUsername(userID).map(user -> {creditCard.setUser(user);
return cardsRepository.save(creditCard);
}).orElseThrow(() -> new NotFoundException("User '" + userID + "' not found"));
This will be much better. Test it yourself and change something if I wrote something wrong
User user = userRepository.findById(userID);
user.getCreditCards().add(creditCard);
creditCard.setUser(user);
userRepository.save(user);
NotFoundException I guess you can handle by yourself.
Update: I had to create an ID for each credit card since if the same ID is assigned on the creation of each new credit card, then program would treat it like if I was the same one that was trying to be updated.

Auto populate created_date, last_modified_date, created_by and last_modified_by in entity : Hibernate with JPA

I am new to Hibernate and JPA. I have several entities, each of which contains following four columns:
1. created_by
2. last_modified_by
3. created_date
4. last_modified_date
I would like these columns to get auto-populated while saving the associated entity.
Two sample entities are as follows:
Entity 1:
#Entity
#Table(name = "my_entity1")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class MyEntity1 implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name")
private String name;
#Column(name = "created_by")
private String createdBy;
#Column(name = "last_modified_by")
private String lastModifiedBy;
#Column(name = "created_date")
private Instant createdDate;
#Column(name = "last_modified_date")
private String lastModifiedDate;
}
Entity 2:
#Entity
#Table(name = "my_entity2")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class MyEntity2 implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "description")
private String description;
#Column(name = "created_by")
private String createdBy;
#Column(name = "last_modified_by")
private String lastModifiedBy;
#Column(name = "created_date")
private Instant createdDate;
#Column(name = "last_modified_date")
private String lastModifiedDate;
}
In this context, I have gone through following posts: How to autogenerate created or modified timestamp field?, How can you make a created_at column generate the creation date-time automatically like an ID automatically gets created?.
I am getting how to capture the dates fields but I cannot understand how to capture created_by and last_modified_by.
Auditing Author using AuditorAware and Spring Security...
To tell JPA about currently logged in user we will need to provide an
implementation of AuditorAware and override getCurrentAuditor()
method. And inside getCurrentAuditor() we will need to fetch currently
logged in user.
Like this:
public class AuditorAwareImpl implements AuditorAware<String> {
#Override
public String getCurrentAuditor() {
return "TestUser";
// Can use Spring Security to return currently logged in user
// return ((User) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()
}
}
Now enable jpa auditing by using #EnableJpaAuditing
#Configuration
#EnableJpaAuditing(auditorAwareRef = "auditorAware")
public class JpaConfig {
#Bean
public AuditorAware<String> auditorAware() {
return new AuditorAwareImpl();
}
}
Look at this to get more details....

Need solution for following scenario in Hibernate many to many mapping

Consider the tables where posts and tags exhibit a many-to-many relationship between each other.
The many-to-many relationship is implemented using a third table called post_tags which contains the details of posts and their associated tags.
Post Model
#Entity
#Table(name = "posts")
public class Post {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotNull
#Size(max = 100)
#Column(unique = true)
private String title;
#NotNull
#Size(max = 250)
private String description;
#NotNull
#Lob
private String content;
#NotNull
#Temporal(TemporalType.TIMESTAMP)
#Column(name = "posted_at")
private Date postedAt = new Date();
#NotNull
#Temporal(TemporalType.TIMESTAMP)
#Column(name = "last_updated_at")
private Date lastUpdatedAt = new Date();
#ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
#JoinTable(name = "post_tags",
joinColumns = { #JoinColumn(name = "post_id") },
inverseJoinColumns = { #JoinColumn(name = "tag_id") })
private Set<Tag> tags = new HashSet<>();
public Post() {
}
public Post(String title, String description, String content) {
this.title = title;
this.description = description;
this.content = content;
}
// Getters and Setters (Omitted for brevity)
}
TAG Model
#Entity
#Table(name = "tags")
public class Tag {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotNull
#Size(max = 100)
#NaturalId
private String name;
#ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
},
mappedBy = "tags")
private Set<Post> posts = new HashSet<>();
public Tag() {
}
public Tag(String name) {
this.name = name;
}
// Getters and Setters (Omitted for brevity)
}
Problem is
I tried to use an existing tags. and insert happened only on posts & posts_tags table.
Initially i'm Get tag(s) with tagName(s). Once you have the Tag object, you can set it in the Post object and save it.
Like this
Post post = new Post("Hibernate Many to Many Example with Spring Boot",
"Learn how to map a many to many relationship using hibernate",
"Entire Post content with Sample code");
// Create two tags
Tag tag1 = tagService.getTag("Spring Boot");
// Add tag references in the post
post.getTags().add(tag1);
postRepository.save(post);
If I do like that, entry is not available in post_tags table.
Tag Repository and Tag Service:
#Repository
public interface TagRepository extends JpaRepository<Tag, Long> {
#Query("select p from Tag p where p.name = :name")
Tag findByName(#Param("name") String name);
}
#Override
public Tag findByName(String name) {
return repository.findByName(name);
}

Resources