Optionals with JPA and Spaghetti code in Springboot API - java

I have a few questions about the use of optionals in an API that I am working on.
First, I am using in my native queries (in my JPA repositories) as return optionals.
It's okay to use them like this?
Also I have a service that checks if one of my entity exits by its id if not it throws a custom exception. What I don't understand is the service can return the Object type, but the JPA query definition is: Optional findById(ID id) ???
public BasicTvShowInfo getBasicTvShowInfoById (Integer idTvShow) throws ResourceNotFoundException {
return basicTvShowInfoRepository.findById(idTvShow).orElseThrow(() -> new ResourceNotFoundException(
"The tv show with the id : " + idTvShow + " was not found."));
}
Finally, I have a validation when I want to post an entity call TvShowReminder. This entity is declared as:
#Entity
#Table(name = "tv_show_reminder")
public class TvShowReminder {
// Attributes
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column (name = "id_tv_show_reminder")
private Integer idTvShowReminder;
#ManyToOne(optional = false)
#JoinColumn(name="id_user")
#NotNull(message = "Provide user {idUser}")
private User user;
#ManyToOne()
#JoinColumn(name= "id_basic_tv_show_info")
private BasicTvShowInfo basicTvShowInfo;
#ManyToOne()
#JoinColumn(name= "id_tv_show_created_by_user")
#OnDelete(action = OnDeleteAction.CASCADE)
private UserTvShow userTvShow;
private Boolean completed;
#Column(name = "current_season")
private Integer currentSeason;
#Column(name = "current_episode")
private Integer currentEpisode;
#Column(name = "personal_rating")
private Integer personalRating;
It has two nested entities, that can be nullable. The BasicTvShowInfo and UserTvShow.
#Entity
#Table(name = "basic_tv_show_info")
public class BasicTvShowInfo {
#Id
#Column(name = "id_basic_tv_show_info")
#NotNull(message = "Provide id (Integer)")
private Integer id;
#Column(name = "original_name")
#JsonProperty("original_name")
#NotNull(message = "Provide original_name (String)")
private String originalName;
}
#Entity
#Table(name = "tv_show_created_by_user")
public class UserTvShow {
// Attributes
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id_tv_show_created_by_user")
private Integer idTvShowCreatedByUser;
#ManyToOne()
#JoinColumn(name= "id_user")
#NotNull(message = "Provide user {idUser}")
private User user;
#Column(name = "name_tv_show")
#NotNull(message = "Provide nameTvShow (String)")
private String nameTvShow;
private String genre;
#Column(name = "production_company")
private String productionCompany;
}
I have a validation method in the service that checks:
if both the BasicTvShowInfo and UserTvShow are null.
if both objects (BasicTvShowInfo and UserTvShow) have an id.
if the logged user already has a reminder with the BasicTvShowInfo id provided.
if the logged user already has a reminder with the UserTvShow id provided.
In all these cases I throw exceptions.
How can I refactor this service method which is working perfectly but I want to write it more elegantly? I really don't like to use so many if-elseif-else and nested ifs. It gets really hard to follow the code.
This is the method:
private void validateExistenceOfTvShowReminder(TvShowReminder tvShowReminder) throws ResourceAlreadyExistsException, BusinessLogicValidationFailure, ResourceNotFoundException {
Optional<TvShowReminder> tvShowReminderOptional = Optional.empty();
String messageError = null;
// If both the basic tv show info object and user tv show object ARE PRESENT -> exception
// If the basic tv show info object id already exists in the reminders table -> exception
// If the user tv show info object id already exists in the reminders table -> exception
// If both are null -> exception.
if(tvShowReminder.getBasicTvShowInfo() != null && tvShowReminder.getUserTvShow() != null){
if(tvShowReminder.getBasicTvShowInfo().getId() != null && tvShowReminder.getUserTvShow().getIdTvShowCreatedByUser() != null)
throw new BusinessLogicValidationFailure("You cant have a reminder that point to a tv show from the system and to a tv show created by the user");
} else if(tvShowReminder.getBasicTvShowInfo() != null){
if(tvShowReminder.getBasicTvShowInfo().getId() != null) {
tvShowReminderOptional = tvShowReminderRepository.findByUserIdAndTvShowId(tvShowReminder.getUser().getIdUser(), tvShowReminder.getBasicTvShowInfo().getId());
messageError = "User already created a tv show reminder with the basicTvShowInfo id : " + tvShowReminder.getBasicTvShowInfo().getId();
}
} else if (tvShowReminder.getUserTvShow() != null){
if(tvShowReminder.getUserTvShow().getIdTvShowCreatedByUser() != null) {
// Validate if the user tv show of the reminder actually belongs to the logged user.
if(Optional.ofNullable(userTvShowService.getUserTvShow(tvShowReminder.getUser().getIdUser(), tvShowReminder.getUserTvShow().getIdTvShowCreatedByUser())).isPresent()) {
tvShowReminderOptional = tvShowReminderRepository.findByUserIdAndTvShowCreatedByUserId(tvShowReminder.getUser().getIdUser(), tvShowReminder.getUserTvShow().getIdTvShowCreatedByUser());
messageError = "User already created a tv show reminder with a userTvShow id : " + tvShowReminder.getUserTvShow().getIdTvShowCreatedByUser();
}
}
} else {
messageError = "To create a tv show reminder you have to provided a basicTvShowInfo id OR a userTvShow id";
throw new BusinessLogicValidationFailure(messageError);
}
// Each query findByUserIdAndTvShowId and findByUserIdAndTvShowCreatedByUserId return an optional with the tv show or an empty optional.
// This method will return true if there is a tv show present in the optional OR returns false if is an empty optional (with null value).
if(tvShowReminderOptional.isPresent()){
throw new ResourceAlreadyExistsException(messageError);
}
}
// Repository if it is of any help is:
#Repository
public interface TvShowReminderRepository extends JpaRepository<TvShowReminder, Integer> {
Page<TvShowReminder> findByUser_IdUser(Pageable pageable, Integer idUser);
List<TvShowReminder> findByUser_IdUser(Integer idUser);
#Query(value = "SELECT * FROM tv_show_reminder WHERE id_user = ?1 and id_basic_tv_show_info = ?2", nativeQuery = true)
Optional<TvShowReminder> findByUserIdAndTvShowId(Integer idUser, Integer idBasicTvShowInfo);
#Query(value = "SELECT * FROM tv_show_reminder WHERE id_user = ?1 and id_tv_show_created_by_user = ?2", nativeQuery = true)
Optional<TvShowReminder> findByUserIdAndTvShowCreatedByUserId(Integer idUser, Integer idTvShowCreatedByUser);
#Query(value = "SELECT * FROM tv_show_reminder WHERE id_user = ?1 and id_tv_show_reminder = ?2", nativeQuery = true)
Optional<TvShowReminder> findByIdTvShowReminderAndUserId(Integer idUser, Integer idTvShowReminder);
}
Sorry for the long post, thank you in advance, if anyone can help me or guide me to the right approach I will appreciate it.

First, I am using in my native queries (in my JPA repositories) as return optionals. It's okay to use them like this?
I don't see why it wouldn't be ok.
Also I have a service that checks if one of my entity exits by its id if not it throws a custom exception. What I don't understand is the service can return the Object type, but the JPA query definition is: Optional findById(ID id) ???
This is fine as well. Spring Data just didn't want to impose the cost of throwing an exception every time an object wasn't found which is why they use the optional approach. You can do whatever you want with that.
Finally, I have a validation when I want to post an entity call TvShowReminder. This entity is declared as:
To refactor validation into more readable form, I would recommend you look into the Jakarta/Java EE Validation API, also called Bean Validation API: https://www.baeldung.com/javax-validation

Related

I'm receiving one object using findAllBy in springBoot

I'm trying to fetch all rows that have the same patient_id, so I'm doing findAllByPatientId. But I'm always receiving one object in the Listinstead of all the rows.
#Entity
#Getter
#Setter
public class MedicalHistory extends BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#ManyToOne
#JoinColumn(name = "operator_id")
private MedicalOperator medicalOperatorId;
#ManyToOne
#JoinColumn(name = "illness_id")
private Illness illnessId;
#ManyToOne
#JoinColumn(name= "patientId")
private Patient patientId;
}
public List<MedicalHistory> getPatientMedicalRecords(PatientDto patientDto) {
Optional<Patient> getPatient = patientRepository.findByNin(patientDto.getNin());
Long patientId = getPatient.get().getPatientId();
return medicalHistoryRepository.findAllByPatientId(patientId);
}
I want to receive multiple rows using the patient_id but instead, I'm always getting one !!.
I tried native query and hibernate but nothing is working.
public interface MedicalHistoryRepository extends JpaRepository<MedicalHistory, Long> {
// List<MedicalHistory> findAllByPatientId(Long id);
ArrayList<MedicalHistory> findMedicalHistoriesByPatientId(Long id);
#Query(value = "SELECT * FROM medical_history WHERE patient_id = id",nativeQuery = true)
List<MedicalHistory> findAllByPatientId(Long id);
}
Now you are requesting "give me medical_history where id = patient_id" and getting only one result row.
You need to add a colon to the query to set a parameter to fix a result
value = "SELECT * FROM medical_history WHERE patient_id = :id"
Look for JPQL, it's java persistancy query language and spring is automatically turning your #query into it. Spring is also supporting spEL you can also have a look to it here : https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query.spel-expressions where you can see than you can grab your parameters ever with ?number or with :name or putting #Param("name") into your parameters definition. As said before there is multiple ways to receive a parameter in you query but certainly not like you previously did.
That's why you don't receive anything.

java jpa one to one relation is always null for one side?

I have two entity booking and travelAgentBooking, booking could exist by itself while travelAgentBooing must have one booking of it.
TABookingEntity is below
#Entity
#ApplicationScoped
#Table(name = "TABooking")
#NamedQuery(name = "TABooking.findAll", query = "SELECT t FROM TABookingEntity t ORDER BY t.id ASC")
public class TABookingEntity {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "TABookingId_seq")
#SequenceGenerator(name = "TABookingId_seq", initialValue = 1, allocationSize = 1)
private Long id;
#OneToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "booking_id", nullable = false)
private BookingEntity flightbooking;
// belong to upstream booking so we just store id here
private Long taxibookingid;
private Long hotelbookingid;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public BookingEntity getFlightbooking() {
return flightbooking;
}
public void setFlightbooking(BookingEntity flightbooking) {
this.flightbooking = flightbooking;
if (flightbooking != null) {
flightbooking.setTravelAgentBooking(this);
}
}
public Long getTaxibookingId() {
return taxibookingid;
}
public void setTaxibookingId(Long taxibookingid) {
this.taxibookingid = taxibookingid;
}
public Long getHotelbookingId() {
return hotelbookingid;
}
public void setHotelbookingId(Long hotelbookingid) {
this.hotelbookingid = hotelbookingid;
}
BookingEntity is below
#Entity
#ApplicationScoped
#Table(name = "booking")
#NamedQueries({ #NamedQuery(name = "Booking.findAll", query = "SELECT b FROM BookingEntity b ORDER BY b.d ASC"),
#NamedQuery(name = "Booking.findByFlight", query = "SELECT b FROM BookingEntity b WHERE b.flight = :flight"),
#NamedQuery(name = "Booking.findByDate", query = "SELECT b FROM BookingEntity b WHERE b.d = :d") })
public class BookingEntity {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "bookingId_seq")
#SequenceGenerator(name = "bookingId_seq", initialValue = 1, allocationSize = 1)
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "customer_id", nullable = false)
private CustomerEntity customer;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "flight_id", nullable = false)
private FlightEntity flight;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "travelAgentBooking_id", nullable = true)
private TABookingEntity travelAgentBooking;
#NotNull
#Column(name = "date")
private Date d;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public CustomerEntity getCustomer() {
return customer;
}
public void setCustomer(CustomerEntity customer) {
this.customer = customer;
if(customer != null)
customer.addBooking(this);
}
public FlightEntity getFlight() {
return flight;
}
public void setFlight(FlightEntity flight) {
this.flight = flight;
}
public Date getDate() {
return new Date(d.getTime());
}
public void setDate(Date d) {
this.d = d;
}
public TABookingEntity getTravelAgentBooking() {
return travelAgentBooking;
}
public void setTravelAgentBooking(TABookingEntity travelAgentBooking) {
this.travelAgentBooking = travelAgentBooking;
}
here is the code I creating booking firstly, and then set it to tabooking.
then I'm trying to update the booking since when it is created, there is no travelAngentBooking for it to associate.
Booking booking = flightService.createBooking(tabooking.getFlightbooking());
tabooking.setFlightbooking(booking);
,,,,,,,,,,,
,,,,,,,,,,,
tabookingService.create(tabooking);
flightService.updateBooking(tabooking.getFlightbooking().getId(), tabooking.getFlightbooking());
After running it the table of travelAgentBooking is perfect.
But booking table column referred to travelAgentBooking is always null for any booking object.
UPDATE:
#PUT
#Path("/{id:[0-9]+}")
#Operation(description = "Update a Booking in the database")
#APIResponses(value = { #APIResponse(responseCode = "200", description = "Booking updated successfully"),
#APIResponse(responseCode = "400", description = "Invalid Booking supplied in request body"),
#APIResponse(responseCode = "404", description = "Booking with id not found"),
#APIResponse(responseCode = "409", description = "Booking details supplied in request body conflict with another existing Booking"),
#APIResponse(responseCode = "500", description = "An unexpected error occurred whilst processing the request") })
#Transactional
public Response updateBooking(
#Parameter(description = "Id of Booking to be updated", required = true) #Schema(minimum = "0") #PathParam("id") Integer id,
#Parameter(description = "JSON representation of Booking object to be updated in the database", required = true) Booking booking) {
Customer customer = customerService.findById(booking.getCustomer().getId())
.orElseThrow(() -> new RestServiceException("We can't found customer", Response.Status.BAD_REQUEST));
if (!customer.equals(booking.getCustomer()))
throw new RestServiceException("use custoemr's own API for it update", Response.Status.BAD_REQUEST);
Flight flight = flightService.findById(booking.getFlight().getId())
.orElseThrow(() -> new RestServiceException("We can't found flight", Response.Status.BAD_REQUEST));
if (!flight.equals(booking.getFlight()))
throw new RestServiceException("use custoemr's own API for it update", Response.Status.BAD_REQUEST);
try {
bookingService.validateBooking(booking);
} catch (ConstraintViolationException ce) {
// Handle bean validation issues
Map<String, String> responseObj = new HashMap<>();
for (ConstraintViolation<?> violation : ce.getConstraintViolations()) {
responseObj.put(violation.getPropertyPath().toString(), violation.getMessage());
}
throw new RestServiceException("Bad Request", responseObj, Response.Status.BAD_REQUEST, ce);
} catch (UniqueFlightWithDateException e) {
// we are updating an existence flight, so ignore this as expected
}
try {
bookingService.update(id);
} catch (ServiceException e) {
Map<String, String> responseObj = new HashMap<>();
responseObj.put("id", "please ensure the id is associated with this number");
throw new RestServiceException("Bad Request", responseObj, Response.Status.NOT_FOUND, e);
}
bookingService.update(id);
return Response.ok(booking).build();
}
BookingEntity update(BookingEntity booking) {
log.info("BookingRepository.update() - Updating " + booking.getId());
em.merge(booking);
return booking;
}
From the original posted code, the problem is that you have two very independent unidirectional relationships and only setting one of them. Since they are independent, the other remains null and can not be anything other than null until it is set.
#OneToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "booking_id", nullable = false)
private BookingEntity flightbooking;
The join column sets a foreign key in the "TABooking" table to point at the bookingEntity. It requires this relationship reference be set to populate that foreign key value. Same thing with:
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "travelAgentBooking_id", nullable = true)
private TABookingEntity travelAgentBooking
It creates its own travelAgentBooking_id foreign key column in the "booking" table that will remain null until you update a booking instance and set this reference. If you only set one side, the other will always remain null in the database.
But there are two problems with the model and your expectations. First, from the comments, you didn't intend this to be two separate relationships and instead expect it to be a single bidirectional relationship. For that, you need a single foreign key, and to pick a side that 'owns' it. The side that owns it controls it:
#OneToOne(mappedBy "flightbooking", fetch = FetchType.LAZY)
private TABookingEntity travelAgentBooking
Using mappedBy tells JPA that the other side owns the relationship. The foreign key column then is only set when you set the TABookingEntity.flightbooking reference and save/merge the TABookingEntity instance.
Second is you are using JSON and so Json serialization and assuming it abides by your object model and the JPA mappings. It does not. JPA annotations are for your persistence provider to tell it how to serialize/deserialize your model into the database but mean nothing for JSON serialization (or xml or any other REST formats). You need to tell your JSON tool how to handle your relationships, and that completely depends on how you are going to be expecting and sending the JSON. There are many tutorials and different strategies to deal with this (see this link for a good primer), but easiest is usually just to pick parts of the graph and exclude them with #JsonIgnore:
#OneToOne(mappedBy "flightbooking", fetch = FetchType.LAZY)
#JsonIgnore
private TABookingEntity travelAgentBooking
This will mean that any JSON you receive representing a booking will have a null travelAgentBooking. So if you need to see or set this relationship, your api would have to send/receive TABookingEntity which would still have the flightbooking reference serialized. I picked this way because flightbooking owns the relationship, so it matches JPA, but it doesn't need to. You can and should figure out what works for your client application and it may be different from the JPA mappings. I would expect that bookings always need to know the TABookingEntity and you'll want that sent to the client, so you might put the #JsonIgnore annotation on the other side. If you do, you'll just have to be sure that when you want to change or add TABookingEntity, that you fix the TABookingEntity.flightbooking reference appropriately so that you don't null out the foreign key.

criteriaBuilder notEqual fuction doesnt return null values for manyToOne class

I have two classes like below
public class User {
#Column(name = "EMP_NAME")
private String name;
#Column(name = "EMP_PASSWORD")
private String password;
#ManyToOne
#JoinColumn(name = "ROLE_ID")
private Role role;
....
}
public class Role {
#column("ROLE_NAME")
private String name;
#column("IS_MASTER")
String isMaster; // 'Y' or 'N'
...
}
I need to get all users which is not master or dont have role. For do this I try something like that but not working.
Predicate nonMaster = cb.notEqual(root.get("role").get("isMaster"), "Y");
cb.and(nonMaster)
but this is not returning users which don't have role value (ROLE_ID == NULL). I also try this but also not work.
Predicate nonMaster = cb.notEqual(root.get("role").get("isMaster"), "Y");
Predicate nullRole = cb.isNull(root.get("role"));
cb.or(nullRole,nonMaster);
How can I make a query like role is null or role.isMaster equal to "N"
for fix this problem I make my query left join. With this I able to take empty values.
Root<User> root = q.from(User.class);
root.join(User_.role,JoinType.LEFT);

What are some ways to pass an Enum value in a pure JPA #Query?

Let's say I want to set a record's column value as "RESOLVED". However, I want to have this value to picked up from an Enum. So the query stays the same regardless if somebody decides the "RESOLVED" to change to "RESOLVE" or something like that. In other words, the developer would not need to edit the JPA query with the new Enum value. Just wanted to know what are some ways to go about it, or if JPA doesn't allow that. I know queries are static but earlier I was able to pass an object inside a query to store the query result fields in it. So I wondered if one could do the same with Enums too.
This is my current strategy: To pass the "RESOLVE" as a #Param "status". However, I'd still like to know how to pass Enums as this feels like a work-around more than answering the question itself.
public interface UpdateStatusStandardDAO extends JpaRepository<DBTableName, Long>
{
#Transactional
#Modifying(clearAutomatically = true)
#Query("UPDATE"
+ " TableName tn"
+ " SET"
+ " tn.status =:status"
+ " WHERE"
+ " tn.field1 =:field1"
+ " AND tn.field2 =:field2"
+ " AND tn.field3 =:field3")
int updateStatus(#Param("status") String status, #Param("field1") Long field1,
#Param("field2") String field2, #Param("field3") String field3);
}
What I know:
1. #Transactional: for update/delete ops. Otherwise, I got org.springframework.dao.InvalidDataAccessApiUsageException.
2. #Modifying(clearAutomatically = true): Because the entity manager does not flush change automagically by default.
For simplicity, we consider field1 as primary key. It's type is Long (can be inferred also from the interface signature).
For pure reference, if none of the fields in the where clause is a key, then SQL workbench might reject your query if the SQL_SAFE_UPDATES options is set. That is because you can't update or delete records without specifying a key (example, primary key) in the where clause.
That's not possible because annotation attribute value must be constant and Enum is not a constant. But you can use java8 interface default method as a trick to solve it. The template code:
public interface TheEntityJpaRepository extends JpaRepository<TheEntity, Long> {
#Modifying(clearAutomatically = true)
#Query("UPDATE TheEntity SET status = :status WHERE id = :value")
int markToStatus(#Param("value") Long value, #Param("status") TheStatusEnum status);
default int markAsResolved(Long value) {
return markToStatus(value, TheStatusEnum.RESOLVED);
}
}
#Entity
public class YourEntity {
...
#Enumerated(EnumType.STRING)
private Status status;
...
}
public enum TheStatusEnum {
RESOLVED,
...
}
But you should know that this way will exposed the markToStatus method.
try this
#Modifying
#Query(value = "update table set status=:#{#status.toString()} where id = :id", nativeQuery = true)
void reset(#Param("id") Long id, #Param("status") TaskStatusEnum status);
you can follow the below process and come out to a solution.
//ENUM structure
#Getter
public enum Status {
RESOLVED(1, "resolved"),
NOT_RESOLVED(2, "not reolved");
private Integer id;
private String name;
Status(Integer id, String name) {
this.id = id;
this.name = name;
}
private static Status getById(Integer id) {
for (Status type : Status.values()) {
if (type.getId() == id) {
return type;
}
}
return null;
}
#Converter(autoApply = true)
public static class TypeConverter implements AttributeConverter<Status, Integer> {
#Override
public Integer convertToDatabaseColumn(Status type) {
return type != null ? type.getId() : null;
}
#Override
public Status convertToEntityAttribute(Integer id) {
return id != null ? Status.getById(id) : null;
}
}
}
//Entity Structure
#AllArgsConstructor
#NoArgsConstructor
#Getter
#Setter
#Builder(builderClassName = "Builder", toBuilder = true)
#Entity
#Table(name = "something")
public class Something {
private static final long serialVersionUID = 1696981698111303897L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false, updatable = false)
protected Long id;
#Column(name = "status_id")
#Convert(converter = Status.TypeConverter.class)
private Status status;
}
//Service method
List<Something> findByIdAndStatus(Long id, Status status);
//impl method
#Override
public List<Something> findByIdAndStatus(Long id, Status status) {
return somthingRepository.findByIdAndStatus(id, status);
}
//repository method
List<Something> findByIdAndStatus(Long id, Status status);
//now call it like this.Note you are passing the enum value here.In this way you can pass any enum value to your JPA query or repository method.
List<Something> yourResult = somethingService.findByIdAndStatus(id,
Status.RESOLVED);

Adding an object from Object A into Object B without creating new object ? HIBERNATE

Lets say I have two objects, say one is a User object and the other is a State Object. The state object is basically the 50 states of America so it doesn't ever have to change. The user object however has a Collection of States where the user has been. So like this:
#Entity
#Table(name="tbl_users")
class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="id", unique=true, nullable = false)
private int id;
#Column(name="user_name")
private String name;
#OneToMany(targetEntity=State.class, orphanRemoval = false)
#Column(name="states")
private Collection<State> states;
//getters and setters
}
and the States entity looks like this:
#Entity
#Table(name="tbl_states")
class State {
#Id
#Column(name="id", unique=true, nullable=false)
private int id;
#Column(name="state")
private String state;
// getters and setters
}
Code for adding user (using hibernate):
public int addUser(User user) {
em.persist(user);
em.flush();
return user.getId();
}
Code for getting state by id:
public State getStateById(int id) {
return em.createQuery("SELECT s FROM State s WHERE s.id =:id, State.class)
.setParameter("id", id)
.getSingleResult();
}
but when I try to create a User and pick several states, I get a PSQLException:
org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "uk_g6pr701i2pcq7400xrlb0hns"
2017-06-21T22:54:35.959991+00:00 app[web.1]: Detail: Key (states_id)=(5) already exists.
I tried looking up the Cascade methods to see if I could use any, but Cascade.MERGE and Cascade.PERSIST seem to do the same thing, and the rest I don't think I need (REMOVE, DETACH, etc). My question is:
How do I add states to the User object without having that error?
This code works:
class Example {
#Test
public void workingTest() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("testPU");
EntityManager em = emf.createEntityManager();
// Creating three states
State alabama = new State(state: 'Alabama');
State louisiana = new State(state: 'Louisiana');
State texas = new State(state: 'Texas');
em.getTransaction().begin();
em.persist(alabama);
em.persist(louisiana);
em.persist(texas);
em.getTransaction().commit();
List<State> states = em.createQuery('FROM State').getResultList();
// Assert only three states on DB
assert states.size() == 3;
User userFromAlabama = new User();
User userFromAlabamaAndTexas = new User();
em.getTransaction().begin();
State alabamaFromDB = em.find(State, alabama.getId());
State texasFromDB = em.find(State, texas.getId());
userFromAlabama.getStates().add(alabamaFromDB);
userFromAlabamaAndTexas.getStates().add(alabamaFromDB);
userFromAlabamaAndTexas.getStates().add(texasFromDB);
em.persist(userFromAlabama);
em.persist(userFromAlabamaAndTexas);
em.getTransaction().commit();
states = em.createQuery('FROM State').getResultList();
// Assert only three states on DB again
assert states.size() == 3;
// Assert one user
User userFromDB = em.find(User, userFromAlabama.getId());
assert userFromDB.getStates().size() == 1;
userFromDB = em.find(User, userFromAlabamaAndTexas.getId());
assert userFromDB.getStates().size() == 2;
}
}
#Entity
#Table(name="tbl_users")
class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id
#Column(name="user_name")
private String name
#ManyToMany
private Collection<State> states = Lists.newArrayList()
// Getters and setters
}
#Entity
#Table(name="tbl_states")
class State {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column(name="state")
private String state;
// Getters and setters
}
You should change your mapping to #ManyToMany!
And you must have 3 tables on DB like this:
TBL_USERS, TBL_STATES and TBL_USERS_TBL_STATES
The TBL_USERS_TBL_STATES table is the default table name that Hibernate uses when a property is annotated with #ManyToMany. If you want to change the tablename of TBL_USERS_TBL_STATES, use the #JoinTable annotation too. See the docs here
With this configuration, you should be able to fetch a State from database, add it to a new User and then persist it. I made a unit test and It works!
In your case it might be better to use a manytomany association with manytomany hibernate dont generate unicity constraint.
Hibernate auto generation scheme behavior is a little bit strange with onetoMany but you can use this workaround.
Try this:
#ManyToMany
#JoinTable(name = "user_state")
private List<State> states;

Categories

Resources