Many-To-Many extra columns Spring JPA - java

Has passed a week and I'm struggling with a problem and it seems that I'm not able to find any answer to it.
I have this structure:
Album model:
#Entity
#Table(name = DatabaseConstants.ALBUM_TABLE_NAME)
#JsonInclude(JsonInclude.Include.NON_EMPTY)
public class Album {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false, length = 100)
private String name;
#Column(nullable = false)
private int imageVersion = 1;
#Column(length = 255)
private String description;
#Column(nullable = false)
private boolean single = false;
#Column(nullable = false)
private Long createdAt;
#Column(nullable = true)
private Long deletedAt;
// Relations
#OneToMany(fetch = FetchType.LAZY)
private List<AlbumView> albumViews;
// Getters and Setters
}
AlbumView model:
#Entity
#Table(name = DatabaseConstants.ALBUM_VIEW_RELATION_TABLE_NAME)
public class AlbumView {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false)
private boolean bigger;
#Column(nullable = false)
private int position;
#Column(nullable = false)
private Long createdAt;
#Column(nullable = true)
private Long deletedAt;
// Relations
#ManyToOne(cascade = CascadeType.PERSIST)
#JoinColumn(name = "album_id")
private Album album;
#ManyToOne(cascade = CascadeType.PERSIST)
#JoinColumn(name = "view_id")
private View view;
// Getters and Setters
}
View model:
#Entity
#Table(name = DatabaseConstants.VIEW_TABLE_NAME)
public class View {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false)
private String name;
#Column(nullable = false)
private Long createdAt;
#Column(nullable = true)
private Long deletedAt;
// Relations
#OneToMany(fetch = FetchType.LAZY)
private List<AlbumView> albumViewList;
// Getters and Setters
}
I need to search a list of albums by an view. I that the query that I need is something like (Using #Query annotation from Spring JPA):
SELECT a, av.bigger, av.position FROM Album a, AlbumView av WHERE av.view.id = ?
But I can't map those two values (bigger, position) because they aren't on the Album model. I need to build an response like:
[
{
id: 1,
name: 'Test Album',
imageVersion: 1,
description: 'Description one',
single: true,
bigger: true,
position: 1
},
{
id: 2,
name: 'Test Album 2',
imageVersion: 1,
description: 'Description two',
single: true,
bigger: false,
position: 2
}
]
As far as I read, I can't use #Transient to help me here (because apparently JPA ignores it and the #Query don't fill those attributes) and I don't know other way to do it.
Thanks.
EDIT 1
I tried the #bonifacio suggestion using the following code in the Repository class of Spring JPA:
#Query("SELECT av.album.id, av.album.name, av.album.imageVersion, av.bigger, av.position FROM AlbumView av WHERE av.view.id = ?1")
List<AlbumView> findByViewId(Long id);
But I got the following response:
[
[
1,
"Test Best Hits",
1,
true,
1
]
]
The values is exactly that, but it is considering that is an array, and not an object like its supposed..

One way to do this is by changing the query to select the AlbumView entity, which contains all the desired fields from both AlbumView and Album entity, and also can use the View entity in the WHERE clause of your query.
In this case, one of the possible solutions would be using the following query:
SELECT av FROM AlbumView av WHERE av.view.id = ?1
Also, I want to note that in your first example you were trying to fetch three different objects per row, like the example bellow:
SELECT a, av.bigger, av.position FROM Album a, AlbumView av WHERE av.view.id = ?
The reason that it does not work is because when you select more than one column (or object), you are not automatically fetching the desired fields in just one line, since Hibernate will convert the result in a Object array (Object[]) for each line of your query's result. So, unless you really need to select multiple objects in one row, it's always recommended to return only one field, or entity on each SELECT you make.

Related

Avoid updating entities with JPA based on timestamp column

let's consider two JPA entities A and B :
#Entity
#Table(name = "A")
#SequenceGenerator(name = "seq_a", allocationSize = 50, initialValue = 1)
public class A {
#Id
#GeneratedValue(generator = "seq_a", strategy = GenerationType.SEQUENCE)
#Column(name = "ID", insertable = false, updatable = false, unique = true, nullable = false)
private Long id;
#Column(name = "CODE")
private String code;
#OneToMany(mappedBy = "a", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
private Set<B> bSet = new HashSet<>();
#Column(name = "CREATED_TIME")
private LocalDateTime createdTime;
//getters + setters
}
#Entity
#Table(name = "B")
#SequenceGenerator(name = "seq_b", allocationSize = 50, initialValue = 1)
public class B {
#Id
#GeneratedValue(generator = "seq_b", strategy = GenerationType.SEQUENCE)
#Column(name = "ID", insertable = false, updatable = false, unique = true, nullable = false)
private Long id;
#Column(name = "SOMETHING")
private String something;
#ManyToOne(optional = false)
#JoinColumn(name = "A_ID", nullable = false, updatable = false)
private A a;
#Column(name = "CREATED_TIME")
private LocalDateTime createdTime;
//getters + setters
}
then consider RestController (springboot context) that have one GET method used for retrieving detail of entity A :
#GetMapping("/{id}")
public ResponseEntity<ADTO> getA(#PathVariable(name = "id", required = true) Long id) {
return aRepository.findById(id)
.map(a -> new ResponseEntity<>(mapper.mapToDomain(a), HttpStatus.OK))
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}
method POST used for creating records of A:
#PostMapping
public ResponseEntity<ADTO> addA(#RequestBody #Valid ADTO aDTO) {
return new ResponseEntity<>(mapper.mapToDomain(a.save(mapper.mapToEntity(ADTO))), HttpStatus.CREATED);
}
and PUT method for updating :
#PutMapping
public ResponseEntity<ADTO> updateA(#RequestBody #Valid ADTO aDTO) {
A a = aRepository.findById(aDTO.getId()).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
ADTO aDTOfound = mapper.mapToDomain(a);
BeanUtils.copyProperties(aDTO, aDTOfound);
return new ResponseEntity<>(mapper.mapToDomain(aRepository.save(mapper.mapToEntity(aDTOfound), HttpStatus.OK)));
}
then let's say that, createdTime attribute is updated everytime the entity is persisted (including created - updating createdTime attribute is done under the hood ...).
Then let's consider scenario, where two users at the same time are retrieving detail of the same entity A (id 1). If user X update the detail from the retrieved content via PUT method, is there any way how to avoid user Y to update the same entity with old content (notice that the createdTime attribute is updated on record with id 1) ? I know that one possible solution, is to make sure that the retrieved createdTime and one from aDTO in update method is the same, but is there any "more" standard solution for this problem ? For example, how to avoid updating entity A (if it was updated previously with USER 1) but let update the childs in Bset which ones for example were not updated by user 1 ...
This is typical problem statement of Optimistic Locking
Optimistic locking is a mechanism that prevents an application from
being affected by the "lost update" phenomenon in a concurrent
environment while allowing some high degree of concurrency at the same
time.
I will solve this problem using #Version, add #Version field in your entity like below
#Entity
public class Student {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#Column(name = "student_name")
private String studentName;
#Column(name = "roll_number")
private String rollNumber;
#Column(name = "version")
#Version
private Long version;
}
In above case When we create an entity for the first time default version value will be zero
On update, the field annotated with #Version will be incremented and subsequent update will same version will fail with OptimisticLockException
The #Version annotation in hibernate is used for Optimistic locking while performing update operation. For more details you can visit https://vladmihalcea.com/jpa-entity-version-property-hibernate/

combine joined columns of a table with one column of another table into JAVA object

I want to combine column of different tables in one entity (object) but im getting a column null even it's not null.
I have two entities called Operation and CanceledOperation:
Operation:
#Entity
#Table(name = "OPERATIONS")
public class Operation{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID", unique = true, nullable = false)
private Long id;
#Column(name = "Date")
private Timestamp date;
#Transient
private String message;
// Other attributes and getters & setters
}
CanceledOperation:
#Entity
#Table(name = "CANCELED_OPERATIONS")
public class CanceledOperation{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private Long id;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "OPERATION_ID", nullable = false)
private Operation operation;
#Column(name = "RAINSON")
private String raison;
//getters & setters
}
I'm trying to join the operation and canceledIperation in order to display the raison for the canceled operation and null non canceled.
this is my native query in my repository:
#query(value = "SELECT op.* , co.raison as raison FROM operations op LEFT JOIN canceled_operations co ON op.ID = co.OPERATION_ID", countQuery = "SELECT count(*) FROM operations op LEFT JOIN canceled_operations co ON op.ID = co.OPERATION_ID")
Page<Operation> getAllOperations(Pageable pageable);
and this is how i call my repository:
final Page<Operation> operations= operationRepository.getAllOperations(pageable);
I tried many solution namely using the Object[] instead of Operation and I have also added an attribute called raison in the operation entity with #transient annotation, but even that I still get the raison is null.
can you please give me a hint to solve this issue.
thank in advance.
Well I found an easy way to do it, instead of doing a complicated query, I have used findAll() of each table then I affect the raison attribute from CanceledOperation to the message attribute of Operation using stream and map the two received lists from repositories.
In the CanceledOperation entity, can you give referencedColumnName = "ID" and try?
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "OPERATION_ID",referencedColumnName = "ID", nullable = false)
private Operation operation;

How to sort the self-reference structure?

In my Spring Boot application, there is the model "Item" which has sefl-reference. I would like to realize sorting by field sortOrder, but I can't understand how to implement it for CRUD operations.
For instance, how to get the value for sortOrder before saving my entity to the repository and how to recalculate sortOrder after updating the entity. Any help will be highly appreciated! Thanks!
#Data
#Entity
#NoArgsConstructor
#AllArgsConstructor
#Table(name = "item")
public class Item {
#Id
#JsonFormat(shape = JsonFormat.Shape.STRING)
#SequenceGenerator(name = "itemSeq", sequenceName = "item_id_seq", allocationSize = 50)
#GeneratedValue(strategy = GenerationType.IDENTITY, generator = "itemSeq")
private Long id;
#NotBlank
#Column(name = "name")
private String name;
#Column(name = "description")
private String description;
#Column(name = "sort_order")
private String sortOrder;
#Column(name = "is_active")
private Boolean isActive;
#ManyToOne
#JoinColumn(name = "parent_id")
#JsonFormat(shape = JsonFormat.Shape.STRING)
#JsonIgnoreProperties({"name", "description", "sortOrder", "isActive", "parent", "children"})
private Item parent;
#OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#Where(clause = "is_active = true")
#Fetch(FetchMode.SUBSELECT)
#OrderBy("sort_order ASC")
private List<Item> children = new ArrayList<>();
}
Suppose the new entity you want to save has a sort_order of x,
First you will have to increment all the other columns which has sort_order >= x. The easiest way you can do it is by using a native query.
#Modifying
#Query("update item set sort_order = sort_order + 1 where sort_order >= ?1", nativeQuery = true)
int incrementSortOrders(int currentSortOrder);
After this, its just a matter of saving your new entity using
repository.save(item)
Update:
If the sort_order is something of the format 1-3-0, 1-3-1, 1-3-2, 1-3-3 to indicate a recursive order, Then this is one possible solution -
Suppose we get the new entity with sort key 1-3-1,
we can still identify the items to be incremented with the > operator as they will be lexicographically larger.
We can calculate the prefix of the current node sequnce as 1-3- by writing the logic in code
Then we can run the following query to increment every other row.
#Modifying
#Query("update item set sort_order = CONCAT(:prefix ,CONVERT(SUBSTRING_INDEX(sort_order, :prefix, -1), UNSIGNED INTEGER) + 1) where sort_order > :currentNodeOrder", nativeQuery = true)
int incrementSortOrders(String prefix, String currentNodeOrder); //prefix is `1-3-` which was pre calculated and current node order is `1-3-1`

how to find id of foreign key in hibernate

I have two entities viz:
State
#Entity
#Table(name = "State")
public class StateEntity {
#Column(name = "id", length = 36, nullable = false, unique = true)
private String id;
#ManyToOne (fetch = FetchType.LAZY)
#JoinColumn(name = "InsurerId", nullable = false)
private InsurerEntity insurer;
#Column(name ="StateName", length = 50, nullable = false)
private String stateName;
//getters and setters
}
Insurer
#Entity
#Table(name = "Insurer")
public class InsurerEntity {
#Column(name = "InsurerId", length = 36, nullable = false, unique = true)
private String insurerId;
#Column(name = "InsurerName", length = 100, nullable = true)
private String insurerName;
#OneToMany(mappedBy = "state", fetch = FetchType.LAZY)
private List<StateEntity> stateEntityList;
//getters and setters
}
the insurer's id gets saved in state database and I want to retrieve it using hibernate query but I cant't seem to find the solution for that
How to write this query SELECT InsurerId FROM State; in Hibernate query using CriteriaBuilder, CriteriaQuery and Root..
If you want to select all Insurers's Ids for all states:
String selectionQuery = "SELECT s.insurer.insurerId FROM State s";
List<String> insurersIds = session.createQuery(selectionQuery).list();
If you want to select the Insurer's Id of a certain state:
String selectionQuery = "SELECT s.insurer.insurerId FROM State s WHERE s.id = :stateId";
String insurerId = (String) session.createQuery(selectionQuery).setParameter("stateId", stateId).getSingleResult(); //This should be placed in a try/catch block to handle org.hibernate.NonUniqueResultException
Edit:
You should update your Insurer entity as Prasad wrote in his answer.
for this you have to map both the class as in put #oneToMany annotation in class InsurerEntity as well
#OneToMany(fetch = FetchType.LAZY,mappedBy="StateEntity", cascade = CascadeType.ALL)
private List< StateEntity > StateEntitys;
and when you fetch states you will also get object of InsurerEntity in it from where you can access it with the getter

Updating values in an Embeddable class

Here's what I'm trying to do...I have a Person
#Entity
#Table(name = "PERSON",
uniqueConstraints = {
#UniqueConstraint(columnNames = {"SSN"})
}
)
#DynamicInsert(true)
#DynamicUpdate(true)
#SelectBeforeUpdate(true)
public class Person implements java.io.Serializable {
private static final long serialVersionUID = 6732775093033061190L;
#Version
#Column(name = "OBJ_VERSION")
private Timestamp version;
#Id
#Column(name = "SSN", length = 12, nullable = false, insertable = true, updatable = true)
private String ssn;
#Column(name = "LAST_NAME", length = 50, nullable = false, insertable = true, updatable = true)
private String lastName;
#Column(name = "FIRST_NAME", length = 30, nullable = false, insertable = true, updatable = true)
private String firstName;
#Column(name = "MIDDLE_NAME", length = 30, nullable = true, insertable = true, updatable = true)
private String middleName;
#OneToOne(fetch = FetchType.LAZY, mappedBy = "person", cascade = CascadeType.ALL)
private Passport passport;
#OneToMany(fetch = FetchType.EAGER, mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Citizenship> citizenship = new HashSet<>();
// Getters and setters left out for brevity
and each person can have one Passport
#Entity
#Table(name = "PASSPORT",
uniqueConstraints = {
#UniqueConstraint(columnNames = {"SSN", "PASSPORT_NUMBER"})
}
)
#DynamicInsert(true)
#DynamicUpdate(true)
#SelectBeforeUpdate(true)
public class Passport implements java.io.Serializable {
private static final long serialVersionUID = 6732775093033061190L;
#Version
#Column(name = "OBJ_VERSION")
private Timestamp version;
#Id
#Column(name = "SSN", length = 12, nullable = false, insertable = true, updatable = true)
private String ssn;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "SSN")
#MapsId
private Person person;
#Column(name = "EXPIRATION_DATE", nullable = true, insertable = true, updatable = false)
private GregorianCalendar expirationDate;
#Column(name = "ISSUING_COUNTRY", nullable = true, insertable = true, updatable = false)
private String issuingCountry;
#Column(name = "PASSPORT_NUMBER", nullable = false, insertable = true, updatable = false)
private String passportNumber;
// Getters and setters left out for brevity
This works, each person can have one Passport and the Passport.ssn is assigned the value of the Person.ssn. This is being done because SSN is a unique identifier and it avoids the need for link tables.
Each person can also have a Citizenship
#Entity
#Table(name = "CITIZENSHIP")
#DynamicInsert(true)
#DynamicUpdate(true)
#SelectBeforeUpdate(true)
public class Citizenship implements java.io.Serializable {
private static final long serialVersionUID = 6732775093033061190L;
#Version
#Column(name = "OBJ_VERSION")
private Timestamp version;
#EmbeddedId
private CitizenshipId citizenshipId;
#Column(name = "DATE_OF_CITIZENSHIP")
private GregorianCalendar dateOfCitizenship;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "SSN")
#MapsId("ssn")
private Person person;
// Getters and setters left out for brevity
I have successfully added a person with a passport and a person without a passport. I have added a third person with a passport and dual citizenship with
// This person has a passport and is a dual citizen.
person = new Person();
person.setSsn("654-89-7531");
person.setFirstName("Lois");
person.setLastName("Lane");
passport = new Passport();
passport.setExpirationDate(new GregorianCalendar());
passport.setIssuingCountry("USA");
passport.setPassportNumber("987654");
Set<Citizenship> citizenshipSet = new HashSet<>();
CitizenshipId citizenshipId = new CitizenshipId();
citizenshipId.setCountry("USA");
Citizenship c = new Citizenship();
c.setDateOfCitizenship(new GregorianCalendar());
c.setCitizenshipId(citizenshipId);
c.setPerson(person);
citizenshipSet.add(c);
citizenshipId = new CitizenshipId();
citizenshipId.setCountry("CAN");
c = new Citizenship();
c.setDateOfCitizenship(new GregorianCalendar());
c.setCitizenshipId(citizenshipId);
c.setPerson(person);
citizenshipSet.add(c);
person.setPassport(passport);
passport.setPerson(person);
session.saveOrUpdate(person);
for(Citizenship citizen : citizenshipSet) {
session.saveOrUpdate(citizen);
}
session.flush();
session.clear();
This looks weird/inefficient to me, but it does work (tips for improvement would be appreciated). But as desired, the Person.ssn is carried into the Citizenship. Here's the problem:
The Person with dual Citizenship currently has citizenship in USA and Canada. Let's assume this is wrong and the Person has citizenship in USA and Mexico, which means the CitizenshipId.country needs to change from "CAN" to "MEX". I have tried a bunch of variations of code like
Criteria citCriteria = session.createCriteria(Citizenship.class);
citCriteria.add(Restrictions.eq("citizenshipId.ssn", "654-89-7531"));
List<Citizenship> citizenship = citCriteria.list();
for(Citizenship c : citizenship) {
if("CAN".equalsIgnoreCase(c.getCitizenshipId().getCountry())) {
session.evict(c);
c.getCitizenshipId().setCountry("MEX");
session.saveOrUpdate(c);
session.flush();
session.clear();
}
}
With "show_sql" on, this doesn't perform an update, even though I can see the values change when debugging. I did try an evict(), then set the country, then saveOrUpdate, which made a new entry (I figured it would).
Phew...the question is: How can the values in an Embeddable class be updated when that class is being used as an EmbeddedId? I feel like I'm close but just missing one thing...
Thanks.
Adding CitizenshipID for reference
#Embeddable
public class CitizenshipId implements Serializable {
private static final long serialVersionUID = 6732775093033061190L;
String ssn;
String country;
// Omitted getters, setters, constructors, hashcode, and equals
Have you tried:
if("CAN".equalsIgnoreCase(c.getCitizenshipId().getCountry())) {
session.evict(c);
c.getCitizenshipId().setCountry("MEX");
c.getPerson().getCitizenship().add(c); // TRY ADDING THIS
session.saveOrUpdate(c);
session.flush();
session.clear();
}
if("CAN".equalsIgnoreCase(c.getCitizenshipId().getCountry())) {
// TRY ADDING THIS ------------------------
//session.evict(c);
CitizenshipId cid = new CitizenshipId();
cid.setSsn(c.getCitizenshipId().getSsn();
cid.setCountry("MEX");
c.setCitizenshipId(cid); // references new CID -- should issue update
// -----------------------------------------
session.saveOrUpdate(c);
session.flush();
session.clear();
}
I removed the .evict due to the description in the API:
Remove this instance from the session cache. Changes to the instance
will not be synchronized with the database. This operation cascades to
associated instances if the association is mapped with
cascade="evict".
Topic How to update values associated with Primary Key in Spring-JPA is inline with what Dan posted above about creating a new object with the old object Id. However, topic Hibernate - update the primary key 'id' column in the table does state that Hibernate doesn't allow updates to primary keys.
The objective here was to create a Person with a(n) SSN, possibly with a Passport, and a Citizenship. SSN is intended to be the primary key so I mapped Person to Passport and Citizenship and used SSN as the JoinColumn.
Person to Passport is a one to one relationship, so that wasn't a problem.
Person to Citizenship is a one to many relationship. This relationship means I had to create an embeddable ID. To make each Citizenship unique the embeddable class CitizenshipId was created with SSN and Country.
Using the accepted answer for Hibernate - update the primary key 'id' column in the table I changed the variations of
Criteria citCriteria = session.createCriteria(Citizenship.class);
citCriteria.add(Restrictions.eq("citizenshipId.ssn", "654-89-7531"));
List<Citizenship> citizenship = citCriteria.list();
for(Citizenship c : citizenship) {
if("CAN".equalsIgnoreCase(c.getCitizenshipId().getCountry())) {
session.evict(c);
c.getCitizenshipId().setCountry("MEX");
session.saveOrUpdate(c);
session.flush();
session.clear();
}
}
to
Query query=session.createQuery("update Citizenship set country = :country1 where ssn = :ssn and country = :country2")
.setString("country1", "MEX").setString("ssn", "654-89-7531").setString("country2", "CAN");
query.executeUpdate();
And an update did occur. Being unable to make an update via typical code (use criteria to get data, update it, then call saveOrUpdate) but being able to make an update via a query doesn't make a whole lot of sense to me. I know that key management is more times than not best left to the database, but when a unique value such as SSN is being used there is no need for another key. If an ID is identified within the code without a generation strategy it stands to reason that the IDs can be updated...JMHO.
Thanks to Dan for his ideas. I hope this topic and its references helps others.

Categories

Resources