I want to input boolean value in ChallengeDto - spring

public class ChallengeDto {
private Long id;
private Category category;
private String title;
private String subTitle;
private boolean like;
private int totalScore;
private int requiredScore;
public ChallengeDto(Long id, Category category, String title, String subTitle, boolean like, int totalScore, int requiredScore) {
this.id = id;
this.category = category;
this.title = title;
this.subTitle = subTitle;
this.like = like;
this.totalScore = totalScore;
this.requiredScore = requiredScore;
}
}
I created challengeDto that include challenge's properties(id, category, title, subtitle, totalScore, requiredScore) and like property(can know that if i like challenge or not).
If I put like button, that information stored challengeLike table.
public class ChallengeLike {
#Id
#GeneratedValue
#Column(name = "challenge_like_id")
private Long id;
#ManyToOne(fetch = LAZY)
#JoinColumn(name = "user_id")
private User user;
#ManyToOne(fetch = LAZY)
#JoinColumn(name = "challenge_id")
private Challenge challenge;
private LocalDateTime createDate;
}
Now I'm trying to write a code to retrieve challengeDto that checks if I clicked like or not, but I'm having a problem... I can't think of what kind of code to make.
#Repository
#RequiredArgsConstructor
public class ChallengeDtoRepository {
private final EntityManager em;
#Transactional
public List<ChallengeDto> findChallenges(Long userId) {
return em.createQuery(
"select new " +
"com.example.candy.controller.challenge.ChallengeDto(c.id,c.category,c.title,c.subTitle,????,c.totalScore,c.requiredScore)" +
" from Challenge c" +
" left join ChallengeLike cl on c.id = cl.challenge.id" +
" and cl.user.id = : userId", ChallengeDto.class)
.setParameter("userId", userId)
.getResultList();
}
}

try to rename the field to likeDone or something different than like, it makes the code ambiguous.
However, just simply do:
cl.likeDone
which means:
return em.createQuery(
"select new " +
"com.example.random.demo.dto.ChallengeDto(c.id,c.category,c.title,c.subTitle,cl.likeDone,c.totalScore,c.requiredScore)" +
" from Challenge c" +
" left join ChallengeLike cl on c.id = cl.challenge.id" +
" where cl.user.id = : userId", ChallengeDto.class)
.setParameter("userId", userId)
.getResultList();
However, try to use JPA if you don't have any mandatory condition to use native query or jpql.
JPA implementation:
#Repository
public interface ChallengeLikeRepository extends JpaRepository<ChallengeLike, Long> {
List<ChallengeLike> findAllByUser_Id(long userId);
}
Just call the repository method from service layer and map to your required dto:
public List<ChallengeDto> findChallenges(Long userId) {
List<ChallengeLike> entities = this.repository.findAllByUser_Id(userId);
return entities.stream().map(this::mapToDto).collect(Collectors.toList());
}
The mapToDto() method converts the entity to corresponding ChallengeDto
private ChallengeDto mapToDto(ChallengeLike x) {
return ChallengeDto.builder()
.category(x.getChallenge().getCategory())
.id(x.getChallenge().getId())
.like(x.isLikeDone())
.requiredScore(x.getChallenge().getRequiredScore())
.subTitle(x.getChallenge().getSubTitle())
.title(x.getChallenge().getTitle())
.totalScore(x.getChallenge().getTotalScore())
.userId(x.getUser().getId())
.build();
}
For your convenience, some properties has been added or changed in some classes. The #Builder annotation has been added to the ChallengeDto class. The rest of the corresponding entity and other classes:
a) ChallengeLike.java
#Entity
#Data
public class ChallengeLike {
#Id
#GeneratedValue
#Column(name = "challenge_like_id")
private Long id;
#ManyToOne
#JoinColumn(name = "user_id")
#JsonIgnoreProperties("challengeLikes")
private User user;
#ManyToOne
#JoinColumn(name = "challenge_id")
#JsonIgnoreProperties("challengeLikes")
private Challenge challenge;
private boolean likeDone;
private LocalDateTime createDate;
}
b) Challenge.java
#Entity
#Data
public class Challenge {
#Id
private Long id;
private Category category;
private String title;
private String subTitle;
private int totalScore;
private int requiredScore;
#OneToMany(mappedBy = "challenge", cascade = CascadeType.ALL)
#JsonIgnoreProperties("challenge")
private List<ChallengeLike> challengeLikes = new ArrayList<>();
}
c) Category.java
public enum Category {
CAT_A,
CAT_B
}
Update
If you want to fetch Challenge entity instead of ChallengeLike and map that to ChallengeDto, first implement ChallangeRepository:
#Repository
public interface ChallengeRepository extends JpaRepository<Challenge, Long> {
}
Add the fetchType to EAGER in Challange Entity class:
#OneToMany(mappedBy = "challenge", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JsonIgnoreProperties("challenge")
private List<ChallengeLike> challengeLikes = new ArrayList<>();
And to map the Challenge to ChallengeDto, you can add another mothod as follows:
private ChallengeDto mapToDto(Challenge x) {
return ChallengeDto.builder()
.category(x.getCategory())
.id(x.getId())
.like(!x.getChallengeLikes().isEmpty() && x.getChallengeLikes().get(0).isLikeDone())
.requiredScore(x.getRequiredScore())
.subTitle(x.getSubTitle())
.title(x.getTitle())
.totalScore(x.getTotalScore())
.userId(x.getUserId()) // if you have user reference in Challenge, remove this otherwise.
.build();
}
finally, to incorporate everything properly, change the caller:
public List<ChallengeDto> findChallenges(Long userId) {
List<Challenge> entities = this.repository.findAll();
List<ChallengeDto> entitiesWithoutChallengeLikes = entities.stream()
.filter(x -> x.getChallengeLikes() == null
|| x.getChallengeLikes().isEmpty())
.map(this::mapToDto).collect(Collectors.toList());
List<ChallengeDto> entitiesInferredFromChallengeLikes = entities.stream()
.filter(x -> x.getChallengeLikes() != null && !x.getChallengeLikes().isEmpty())
.flatMap(x -> x.getChallengeLikes().stream())
.map(this::mapToDto)
.collect(Collectors.toList());
entitiesInferredFromChallengeLikes.addAll(entitiesWithoutChallengeLikes);
return entitiesInferredFromChallengeLikes;
}
Final Update
Well, I finally understood properly what you expected. Adopt the following changes to the previous solution and you will get exactly what you want.
Change the 2 occurrence of the following in the findChallanges method:
.map(this::mapToDto)
To:
.map(x -> mapToDto(x, userId))
And the two mapToDto functions will be changed to follows:
private ChallengeDto mapToDto(ChallengeLike x, long userId) {
return ChallengeDto.builder()
.category(x.getChallenge().getCategory())
.id(x.getChallenge().getId())
.like(x.getUser().getId() == userId && x.isLikeDone())
.requiredScore(x.getChallenge().getRequiredScore())
.subTitle(x.getChallenge().getSubTitle())
.title(x.getChallenge().getTitle())
.totalScore(x.getChallenge().getTotalScore())
.userId(x.getUser().getId())
.build();
}
private ChallengeDto mapToDto(Challenge x, long userId) {
return ChallengeDto.builder()
.category(x.getCategory())
.id(x.getId())
.like(false)
.requiredScore(x.getRequiredScore())
.subTitle(x.getSubTitle())
.title(x.getTitle())
.totalScore(x.getTotalScore())
.userId(userId)
.build();
}

Related

JPA lazy initialization error with #OneToMany #EmbeddedId

In Sprinboot/JPA I defined an entity with one-to-may association as follows:
#Entity
#Table(name = "useraccount", catalog = "useraccount")
public class UserAccount implements Serializable
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
//other stuff...
#OneToMany(mappedBy ="tokenId.user", cascade = {CascadeType.REMOVE, CascadeType.MERGE, CascadeType.REFRESH}, orphanRemoval =true, fetch=FetchType.LAZY)
private Set<SecureToken> tokens = new HashSet<>();
public Set<SecureToken> getTokens()
{
return this.tokens;
}
//other getter and setter
}
The SecureToken entity:
#Entity
#Table(name = "secureToken", catalog = "useraccount")
public class SecureToken implements Serializable
{
#EmbeddedId
public SecureTokenId tokenId= new SecureTokenId();
#Column(unique = true)
private String token;
private Timestamp isConsumed;
#CreationTimestamp
#Column(updatable = false)
private Timestamp timestamp;
#Column(updatable = false)
#Basic(optional = false)
private Timestamp expireAt;
#MapsId("user_id")
#JoinColumn(name = "user_id", referencedColumnName ="id")
#ManyToOne
private UserAccount user;
public SecureToken(UserAccount user, String token, String tokenType, Timestamp timestamp, Timestamp expire)
{
super();
this.token=token;
this.tokenId.setTokenType(tokenType);
this.tokenId.setUser(user);
this.timestamp=timestamp;
this.expireAt=expire;
this.isExpired=false;
}
}
The SecureTokenId:
#Embeddable
public class SecureTokenId implements Serializable
{
#Column(name="tokenType")
private String tokenType;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id")
private UserAccount user;
public SecureTokenId()
{
super();
}
public SecureTokenId(String tokenType)
{
//this.user_id=user_id;
this.tokenType=tokenType;
}
#Override
public boolean equals(Object o)
{
if (o == null || getClass() != o.getClass())
return false;
SecureTokenId that = (SecureTokenId) o;
return Objects.equals(this.tokenType, that.tokenType) &&
Objects.equals(this.user.getId(), that.user.getId());
}
#Override
public int hashCode() {
return Objects.hash(tokenType, this.user.getId());
}
public void setTokenType(String tokenType)
{
this.tokenType=tokenType;
}
public String getTokenType()
{
return this.tokenType;
}
public void setUser(UserAccount user)
{
this.user=user;
}
public UserAccount getUser()
{
return this.user;
}
public Long getTokenId()
{
return this.user.getId();
}
}
But calling the method getToken() of entity UserAccount gets the famous "LazyInitializationException". I generally use Hibernate.initialize, but with this configuration I cannot get rid of the problem.
This how I create a token within a #Service annoted SecureTokenService class.
#Override
#Transactional
public SecureToken generateToken(UserAccount user, String tokenType)
{
byte[] random = new byte[64];
new SecureRandom().nextBytes(random);
Timestamp timestamp = java.sql.Timestamp.valueOf(LocalDateTime.now());
LocalDateTime expire= LocalDateTime.now().plusHours(12);
SecureToken token = new SecureToken(new SecureTokenId(user, tokenType),Base64.encodeBase64URLSafeString(random),
timestamp, Timestamp.valueOf(expire));
return token;
}
Then in the UserService class (#Service annotated) I try to create a token:
SecureToken token = secureTokenService.generateToken(user, type);
secureTokenService.save(token);
user.addSecureToken(token); //Error
this.save(user)
When I try to associate the token with the user the error is thrown. Without that statement, the application seems working but even with "spring.jpa.open-in-view = false" in application.properties calling user.getTokens() rises the lazy initialization error.
In parent child relationship, you didn't declare any parent reference from child side.
In the parent side (UserAccount), you declared as follows
#OneToMany(mappedBy ="user"....
Which means your child side (SecureToken) there is no such property named user.
To get rid of this situation,
First you need to declare user inside of SecureToken / SecureTokenId. From your definition, you declared user_id inside SecureTokenId, instead declare user inside SecureTokenId.
...
public class SecureTokenId ... {
...
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id")
private UserAccount user;
...
}
Then in the UserAccount declare the #OneToMany as follows
#OneToMany(mappedBy ="tokenId.user"...
private Set<SecureToken> tokens;

Aggregate function on related entities in jpa + Spring

I want implementation of method should to find all items,
that have a rating lower than passed as an argument. Using reviews associated with each item to calculate item rating.
Following are my classes
#Entity
public class Item {
#Id
#GeneratedValue
private Long id;
#Column(length = 100)
#NotEmpty
private String title;
#Column(length = 200)
private String description;
#OneToMany(mappedBy = "item", cascade = CascadeType.ALL, orphanRemoval = true)
Set<Review> reviews = new HashSet<>();
public Item() {
}
public Item(String title, String description) {
this.title = title;
this.description = description;
}
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
public String getDescription() {
return description;
}
public Set<Review> getReviews() {
return reviews;
}
}
#Entity
public class Review {
#Id
#GeneratedValue
private Long id;
#Min(0)
#Max(10)
private Integer rating;
#Length(max=200)
private String comment;
#ManyToOne(optional = false)
private Item item;
#ManyToOne(optional = false)
private User author;
}
#Repository
#Transactional
public class ItemRepository {
#PersistenceContext
EntityManager entityManager;
public List<Item> findItemsWithAverageRatingLowerThan(Integer rating) {
return new ArrayList<>();
}
I want to find all the items who has average rating of supplied parameter.
Average of rating should be calculated from set of reviews present in Item entity
You can have a look on the following url:
JPA / Hibernate One to One Mapping Example with Spring Boot

Issue mapping fields ModelMapper

I use DTO and modelMapper in order not to make visible some fields.
I have a CategoryEntity that can have subcategories
public class CategoryEntity {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
private Long id;
#Column(length = 30, nullable = false)
private String categoryKeyId;
#Column(nullable = false)
private String name;
#ManyToOne(optional = true, fetch = FetchType.LAZY)
#JoinColumn(name="parent_id", nullable=true)
private CategoryEntity parentCategory;
// allow to delete also subcategories
#OneToMany(mappedBy="parentCategory", cascade = CascadeType.ALL)
private List<CategoryEntity> subCategories;
}
When i create a category I use a model:
#Getter #Setter
public class CategoryRequestModel {
private String name;
private String parentCategoryKeyId;
}
In this model i want parentCategoryKeyId to match with the categoryKeyId of the parent.
For example if i create a "top" category :
{
"name": "topCategory"
}
It returns me :
{
"categoryKeyId": "jUcpO27Ch2YrT2zkLr488Q435F8AKS",
"name": "topCategory",
"subCategories": null
}
When i do this :
{
"name": "sub",
"parentCategoryKeyId": "jUcpO27Ch2YrT2zkLr488Q435F8AKS"
}
In my Controller, i pass the rest object to a DTO Layer which calls a service :
public CategoryRestResponseModel createCategory(#RequestBody CategoryRequestModel categoryRequestModel) {
CategoryRestResponseModel returnValue = new CategoryRestResponseModel();
if( categoryRequestModel.getName().isEmpty())
throw new NullPointerException(ErrorMessages.MISSING_REQUIRED_FIELDS.getErrorMessage());
ModelMapper modelMapper = new ModelMapper();
CategoryDto categoryDto = modelMapper.map(categoryRequestModel, CategoryDto.class);
CategoryDto createdCategory = categoryService.createCategory(categoryDto);
returnValue = modelMapper.map(createdCategory, CategoryRestResponseModel.class);
return returnValue;
}
My CategoryDto is a basic POJO :
#Getter #Setter
public class CategoryDto implements Serializable {
#Getter(AccessLevel.NONE)
private static final long serialVersionUID = 1L;
private String categoryKeyId;
private String parentCategoryKeyId;
private String name;
private CategoryDto parentCategory;
private List<CategoryDto> subCategories;
}
In my Service :
public CategoryDto createCategory(CategoryDto categoryDto) {
//1. Create an empty object to return
System.out.println("Hello World");
CategoryDto returnValue = new CategoryDto();
System.out.println("CategoryDto: " + categoryDto);
// check if category exists
if (categoryRepository.findByName(categoryDto.getName()) != null)
throw new ApplicationServiceException("Record already in Database");
ModelMapper modelMapper = new ModelMapper();
CategoryEntity categoryEntity = modelMapper.map(categoryDto, CategoryEntity.class);
// Generate categoryKeyId
String categoryKeyId = utils.generateCategoryKeyId(30);
categoryEntity.setCategoryKeyId(categoryKeyId);
System.out.println("categoryDto parentCategory: " + categoryDto.getParentCategory());
System.out.println("CategoryDto: " + categoryDto);
if(categoryDto.getParentCategoryKeyId() != null) {
CategoryEntity parentCategory = categoryRepository.findByCategoryKeyId(categoryDto.getParentCategoryKeyId());
categoryEntity.setParentCategory(parentCategory);
System.out.println("CategoryEntity: " + categoryEntity);
System.out.println("parentCategory: " + parentCategory);
}
CategoryEntity storedCategory = categoryRepository.save(categoryEntity);
returnValue = modelMapper.map(storedCategory, CategoryDto.class);
return returnValue;
}
My issue is that I would like to save the subcategory and retrieve the ID that match the categoryKeyId ...
In the database my entry should be like this
My First entry should have:
id = 1 - parent_id = null, category_key_id = jUcpO27Ch2YrT2zkLr488Q435F8AKS, name = topCategory ...
AND :
id = 2 - parent_id = 1 , category_key_id = "another generated key", name= sub
Unfortunatelly I just persist the id, the categorykeyid and the name.
I removed id from CategoryDto and i obtain : 1) Converter org.modelmapper.internal.converter.NumberConverter#348fc3d8 failed to convert java.lang.String to java.lang.Long.
I solved it in a "dirty" way.
I just changed my object in entry and added a long id.
It gives me :
#Getter #Setter
public class CategoryRequestModel {
private Long id;
private String name;
private String parentCategoryKeyId;
}

Multi column search using Specifications Spring Data Jpa within associated entity?

I am taking this question Perform multi column search on Date, Integer and String Data type fields of Single Table? and This method must return a result of type Specification<Employee> in Java 8 further ahead.
Actually I wanted to search within association entity as well as a part of global search. Will that be possible using JPA 2 Specifications API ?
I've Employee and Department #OneToMany bi-directional relationship.
Employee.java
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
#Entity
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "EMPLOYEE_ID")
private Long employeeId;
#Column(name = "FIRST_NAME")
private String firstName;
#Column(name = "LAST_NAME")
private String lastName;
#Column(name = "EMAIL_ID")
private String email;
#Column(name = "STATUS")
private String status;
#Column(name = "BIRTH_DATE")
private LocalDate birthDate;
#Column(name = "PROJECT_ASSOCIATION")
private Integer projectAssociation;
#Column(name = "GOAL_COUNT")
private Integer goalCnt;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "DEPT_ID", nullable = false)
#JsonIgnore
private Department department;
}
Department.java
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
#Entity
public class Department implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "DEPT_ID")
private Long departmentId;
#Column(name = "DEPT_NAME")
private String departmentName;
#Column(name = "DEPT_CODE")
private String departmentCode;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "department")
#JsonIgnore
private Set<Employee> employees;
}
and I saved Data like below.
MyPaginationApplication.java
#SpringBootApplication
public class MyPaginationApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(MyPaginationApplication.class, args);
}
#Autowired
private EmployeeRepository employeeRepository;
#Autowired
private DepartmentRepository departmentRepository;
#Override
public void run(String... args) throws Exception {
saveData();
}
private void saveData() {
Department department1 = Department.builder()
.departmentCode("AD")
.departmentName("Boot Depart")
.build();
departmentRepository.save(department1);
Employee employee = Employee.builder().firstName("John").lastName("Doe").email("john.doe#gmail.com")
.birthDate(LocalDate.now())
.goalCnt(1)
.projectAssociation(2)
.department(department1)
.build();
Employee employee2 = Employee.builder().firstName("Neha").lastName("Narkhede").email("neha.narkhede#gmail.com")
.birthDate(LocalDate.now())
.projectAssociation(4)
.department(department1)
.goalCnt(2)
.build();
Employee employee3 = Employee.builder().firstName("John").lastName("Kerr").email("john.kerr#gmail.com")
.birthDate(LocalDate.now())
.projectAssociation(5)
.department(department1)
.goalCnt(4)
.build();
employeeRepository.saveAll(Arrays.asList(employee, employee2, employee3));
}
}
EmployeeController.java
#GetMapping("/employees/{searchValue}")
public ResponseEntity<List<Employee>> findEmployees(#PathVariable("searchValue") String searchValue) {
List<Employee> employees = employeeService.searchGlobally(searchValue);
return new ResponseEntity<>(employees, HttpStatus.OK);
}
EmployeeSpecification.java
public class EmployeeSpecification {
public static Specification<Employee> textInAllColumns(Object value) {
return (root, query, builder) -> builder.or(root.getModel().getDeclaredSingularAttributes().stream()
.filter(attr -> attr.getJavaType().equals(value.getClass()))
.map(attr -> map(value, root, builder, attr))
.toArray(Predicate[]::new));
}
private static Object map(Object value, Root<?> root, CriteriaBuilder builder, SingularAttribute<?, ?> a) {
switch (value.getClass().getSimpleName()) {
case "String":
return builder.like(root.get(a.getName()), getString((String) value));
case "Integer":
return builder.equal(root.get(a.getName()), value);
case "LocalDate":
return builder.equal(root.get(a.getName()), value);//date mapping
default:
return null;
}
}
private static String getString(String text) {
if (!text.contains("%")) {
text = "%" + text + "%";
}
return text;
}
}
When I hit the /employees/{searchValue}, I want searching to be happened in Department Table along with Employee table (may be using Joins something like that). Is that possible ? If yes, how can we do that ?
Or:
Will this be good approach to put like here? Got reference from Using #Query
#Query("SELECT t FROM Todo t WHERE " +
"LOWER(t.title) LIKE LOWER(CONCAT('%',:searchTerm, '%')) OR " +
"LOWER(t.description) LIKE LOWER(CONCAT('%',:searchTerm, '%'))")
List<Todo> findBySearchTerm(#Param("searchTerm") String searchTerm);
Any pointers?
If you take a look at my post actually I have a solution for join
#Override
public Specification<User> getFilter(UserListRequest request) {
return (root, query, cb) -> {
query.distinct(true); //Important because of the join in the addressAttribute specifications
return where(
where(firstNameContains(request.search))
.or(lastNameContains(request.search))
.or(emailContains(request.search))
)
.and(streetContains(request.street))
.and(cityContains(request.city))
.toPredicate(root, query, cb);
};
}
private Specification<User> firstNameContains(String firstName) {
return userAttributeContains("firstName", firstName);
}
private Specification<User> lastNameContains(String lastName) {
return userAttributeContains("lastName", lastName);
}
private Specification<User> emailContains(String email) {
return userAttributeContains("email", email);
}
private Specification<User> userAttributeContains(String attribute, String value) {
return (root, query, cb) -> {
if(value == null) {
return null;
}
return cb.like(
cb.lower(root.get(attribute)),
containsLowerCase(value)
);
};
}
private Specification<User> cityContains(String city) {
return addressAttributeContains("city", city);
}
private Specification<User> streetContains(String street) {
return addressAttributeContains("street", street);
}
private Specification<User> addressAttributeContains(String attribute, String value) {
return (root, query, cb) -> {
if(value == null) {
return null;
}
ListJoin<User, Address> addresses = root.joinList("addresses", JoinType.INNER);
return cb.like(
cb.lower(addresses.get(attribute)),
containsLowerCase(value)
);
};
}
private String containsLowerCase(String searchField) {
return "%" + searchField.toLowerCase() + "%";
}
Here you can see how I search the users by their address columns (city and street).
EDIT: Also you cannot use the #Query annotation that much dinamically (you van insert parameter values dinamically, but not parameters. That's where Specificaion is handy)
EDIT2: I know this is not the 2.x.x Spring version, but 1.5.x, but the idea is the same for joins.

org.hibernate.PropertyAccessException: Could not set field value [1] value by reflection

Hi guys I am new to Spring and I am getting this error in my project:
org.hibernate.PropertyAccessException: Could not set field value [1] value by
reflection : [class com.**.domain.identities.NurseAgencyIdentity.agencyId]
setter of com.**.domain.identities.NurseAgencyIdentity.agencyId
There are some classes involved in this process: Nurse , Agency, Named(abstract), NurseAgency and NurseAgencyIdentity. There is a many-to-many relationship between Nurse--Agency with an extra column nurse record. The Named class is an abstract class that contains the fields id and name and is being used by many tables in my design being id the identifier of the descendant table. To implement the many-to-many I had to use the #Embeddable annotation in the last class NurseAgencyIdentity which is the id of my NurseAgency join table. Here is the code:
NurseAgencyIdentity
#Embeddable
#Data
public class NurseAgencyIdentity implements Serializable {
#Column(name="nurse_id")
private Long nurseId;
#Column(name="agency_id")
private Long agencyId;
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
NurseAgencyIdentity that = (NurseAgencyIdentity) o;
return Objects.equals(nurseId, that.nurseId) &&
Objects.equals(agencyId, that.agencyId);
}
#Override
public int hashCode() {
return Objects.hash(nurseId, agencyId);
}
}
NurseAgency
#Entity
#Data
public class NurseAgency {
#EmbeddedId
private NurseAgencyIdentity id;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("nurseId")
private Nurse nurse;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("agencyId")
private Agency agency;
private String nurseRecord;
}
Nurse
#Entity
#Data
public class Nurse {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
#Enumerated(EnumType.STRING)
private License license;
#OneToMany(mappedBy = "nurse", cascade = CascadeType.ALL, orphanRemoval = true)
private List<NurseAgency> agencies = new ArrayList<>();
// need the extra column
public void addAgency(Agency agency) {//, String nurseRecord) {
NurseAgency nurseAgency = new NurseAgency();
nurseAgency.setAgency(agency);
nurseAgency.setNurse(this);
//nurseAgency.setNurseRecord(nurseRecord);
agency.getNurses().add(nurseAgency);
}
public void removeAgency(Agency agency) {
for (Iterator<NurseAgency> iterator = agencies.iterator(); iterator.hasNext(); ) {
NurseAgency nurseAgency = iterator.next();
if (nurseAgency.getNurse().equals(this) && nurseAgency.getAgency().equals(agency)){
iterator.remove();
nurseAgency.getAgency().getNurses().remove(nurseAgency);
nurseAgency.setNurse(null);
nurseAgency.setAgency(null);
}
}
}
#Override
public String toString() {
return id + " " + firstName + " " + middleName + " " + lastName;
}
}
Named
#MappedSuperclass
#EntityListeners(AuditingEntityListener.class)
#Data
public abstract class Named implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
Agency
#Entity
#Data
public class Agency extends Named {
private String description;
#OneToMany(mappedBy = "agency", cascade = CascadeType.ALL, orphanRemoval = true)
private List<NurseAgency> nurses = new ArrayList<>();
}
And I am having this error when trying to seed the join table:
BootStrapData
#Component
public class BootStrapData implements CommandLineRunner {
#Autowired
private final NurseRepository nurseRepository;
#Autowired
private final AgencyRepository agencyRepository;
private final NurseAgencyRepository nurseAgencyRepository;
public BootStrapData(NurseRepository nurseRepository, AgencyRepository agencyRepository, NurseAgencyRepository nurseAgencyRepository) {
this.nurseRepository = nurseRepository;
this.agencyRepository = agencyRepository;
this.nurseAgencyRepository = nurseAgencyRepository;
}
#Override
public void run(String... args) throws Exception {
System.out.println("Loading agencies ");
ArrayList<Agency> agencies = GetAgencies();
System.out.println("Loading Nurses ");
ArrayList<Nurse> nurses = GetNurses(agencies);
nurses.stream().forEach( n -> nurseRepository.save(n));
agencies.stream().forEach( a -> agencyRepository.save(a));
//Nurses Agencies
ArrayList<NurseAgency> nurseAgencies = new ArrayList<>(1);
nurseAgencies.addAll(SetNurseAndAgencies(nurses.get(0), new Agency[]{agencies.get(0), agencies.get(1), agencies.get(2)}));
nurseAgencies.addAll(SetNurseAndAgencies(nurses.get(1), new Agency[]{agencies.get(0), agencies.get(1)}));
nurseAgencies.addAll(SetNurseAndAgencies(nurses.get(2), new Agency[]{agencies.get(1), agencies.get(2)}));
for (int i=0; i<nurseAgencies.size();i++){
nurseAgencyRepository.save(nurseAgencies.get(i)); // I've got the error in first iteration in this line
}
}
private ArrayList<Agency> GetAgencies() {
ArrayList<Agency> agencies = new ArrayList<>(3);
Agency a1 = new Agency();
a1.setName("Agency 1");
agencies.add(a1);
Agency a2 = new Agency();
a2.setName("Agency 2");
agencies.add(a2);
Agency a3 = new Agency();
a3.setName("Agency 3");
agencies.add(a3);
return agencies;
}
private ArrayList<Nurse> GetNurses(ArrayList<Agency> agencies) {
ArrayList<Nurse> nurses = new ArrayList<>(3);
Nurse n1 = new Nurse();
n1.setFirstName("Mario");
n1.setLastName("Perez");
nurses.add(n1);
Nurse n2 = new Nurse();
n2.setFirstName("Luis");
n2.setLastName("Ruiz");
nurses.add(n2);
Nurse n3 = new Nurse();
n3.setFirstName("Maria");
n3.setLastName("Crez");
nurses.add(n3);
return nurses;
}
private ArrayList<NurseAgency> SetNurseAndAgencies(Nurse nurse, Agency[] agencies) {
ArrayList<NurseAgency> nurseagencies = new ArrayList<>(agencies.length);
for (int i=0; i<agencies.length; i++){
NurseAgency na = new NurseAgency();
na.setNurse(nurse);
na.setAgency(agencies[i]);
na.setNurseRecord(nurse.getFirstName() + agencies[i].getName());
nurseagencies.add(na);
}
return nurseagencies;
}
}
Where is the problem?
Try changing the NurseAgencyIdentity declaration on NurseAgency from:
#EmbeddedId
private NurseAgencyIdentity id;
to:
#EmbeddedId
private NurseAgencyIdentity id = new NurseAgencyIdentity();
I didn't see the full stack trace but the root cause can be a NullPointerException when hibernate tries to set fields (generated agencyId [ 1 ] in your case) via reflection on NurseAgencyIdentity and it's null.
See org.hibernate.tuple.entity.AbstractEntityTuplizer#getIdentifier

Resources