Spring JPA Locking Concept - spring

Anybody have any idea how to use Locking with UPDATE statement in Spring JPA, so that only one thread can UPDATE the record at a time?
Here, I am trying to update the availability of table name aircraft_route and before updating, I want to take the lock of that row so that no other thread can update it at the same time.
Error: Illegal attempt to set lock mode on a native SQL query
Any help would be appreciated.
Thanks
AirCraftRoute.java
#Builder
#Setter
#Getter
#AllArgsConstructor
#NoArgsConstructor
#Entity(name = "aircraft_route")
public class AirCraftRoute implements Serializable
{
#EmbeddedId
private AirCraftRoutePK pk = new AirCraftRoutePK();
#ManyToOne
#MapsId("airCraftId")
#JoinColumn(name = "aircraft_id")
private AirCraft airCraft;
#ManyToOne
#MapsId("routeId")
#JoinColumn(name = "route_id")
private Route route;
#Column(name = "journey_date")
private Date journeyDate;
#Column(name = "departure_time")
private Time departureTime;
#Column(name = "arrival_time")
private Time arrivalTime;
#Column(name = "fare")
private float fare;
#Column(name = "availability")
private int availability;
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof AirCraftRoute)) return false;
AirCraftRoute that = (AirCraftRoute) o;
return Float.compare(that.getFare(), getFare()) == 0 &&
Objects.equals(getAirCraft(), that.getAirCraft()) &&
Objects.equals(getRoute(), that.getRoute()) &&
Objects.equals(getJourneyDate(), that.getJourneyDate()) &&
Objects.equals(getDepartureTime(), that.getDepartureTime()) &&
Objects.equals(getArrivalTime(), that.getArrivalTime());
}
#Override
public int hashCode() {
return Objects.hash(getAirCraft(), getRoute(), getJourneyDate(), getDepartureTime(), getArrivalTime(), getFare());
}
}
AirCraftRouteRepository.java
#Repository
public interface AirCraftRouteRepository extends JpaRepository<AirCraftRoute,Integer> {
#Transactional
#Query(value = "SELECT ar.availability FROM aircraft_route ar WHERE ar.aircraft_id = :airCraftId AND ar.route_id = :routeId FOR UPDATE",nativeQuery = true)
int getAvailability(#Param("airCraftId") Long airCraftId, #Param("routeId") Long routeId);
#Modifying
#Transactional
#Lock(LockModeType.PESSIMISTIC_WRITE)
#Query(value = "UPDATE aircraft_route ar SET ar.availability = ar.availability - :numberOfTickets WHERE ar.aircraft_id = :airCraftId AND ar.route_id = :routeId AND ar.availability > 0",nativeQuery = true)
int updateAvailability(#Param("numberOfTickets") int numberOfTickets,#Param("airCraftId") Long airCraftId, #Param("routeId") Long routeId);
}

I think you don't need the Lock at all for native SQL. You can FOR UPDATE directly in the native query. Like this
#Transactional
#Query(value = "SELECT * FROM aircraft_route ar WHERE ar.aircraft_id = :airCraftId AND ar.route_id = :routeId FOR UPDATE",nativeQuery = true)
List<Integer> getAvailability(#Param("airCraftId") Long airCraftId, #Param("routeId") Long routeId);

I think you can simply avoid to use native query for that simple syntax:
#Transactional
#Lock(LockModeType.PESSIMISTIC_WRITE)
#Query(value = "SELECT * FROM AircraftRoute ar WHERE ar.id = :airCraftId AND ar.routeId = :routeId")
List<Integer> getAvailability(#Param("airCraftId") Long airCraftId, #Param("routeId") Long routeId);
with something like above. You have to use right syntax with entity name and field but then you can use the #Lock Spring annotation and its db lock management instead of manual handling.

Related

I want to input boolean value in ChallengeDto

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();
}

Hibernate - Spring - ConstraintViolationException - UniqueConstraint

I'm trying to make some fixtures for my Profile model but every time I'm trying to save it "again" after I did an update, I get this message:
nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
This is my Profile class:
#Entity
#Data
#Builder
#ToString(of = {"birthday", "discordId", "description", "spokenLanguages"})
#NoArgsConstructor
#AllArgsConstructor
#Table(name = "profile", uniqueConstraints = #UniqueConstraint(columnNames = "discordId"))
public class Profile implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int idProfile;
private Date birthday;
#Column(name="discordId", insertable=true, updatable=false)
private String discordId;
private String description;
#ElementCollection(fetch = FetchType.EAGER)
private Set<String> spokenLanguages = new LinkedHashSet<String>();
#JsonIgnore
#OneToMany(fetch = FetchType.EAGER)
private Set<ProfileGame> profileGames = new LinkedHashSet<>();
#OneToOne(mappedBy = "profile", cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = false)
private User user;
#ManyToOne
private TimeSlot timeSlot;
}
Here is the call:
#Order(7)
#Test
void fillProfileGame() {
List<Profile> profileList = this.profileRepository.findAll();
for (Profile profile : profileList) {
List<Game> gameList = this.gameRepository.findAll();
Collections.shuffle(gameList);
int rndNbGame = new Random().ints(1, 5).findFirst().getAsInt();
for (int i = 1; i <= rndNbGame; i++) {
int rndLevel = new Random().ints(1, 100).findFirst().getAsInt();
int rndRanking = new Random().ints(1, 3000).findFirst().getAsInt();
Game rndGame = gameList.get(0);
gameList.remove(0);
ProfileGame profileGames = new ProfileGame(profile, rndGame, "level-" + rndLevel,
"ranking-" + rndRanking);
this.profileGameRepository.save(profileGames);
this.gameRepository.save(rndGame);
}
this.profileRepository.save(profile);
}
}
So what I understand is that Hibernate won't let me update this object because it has a unique contraint field ?
How do we proceed when we want a field to be unique and still being able to update other fields ?
From the code snippet, what I see is that there are some unique constraints applied on the column 'discordId'.
#Table(name = "profile", uniqueConstraints = #UniqueConstraint(columnNames = "discordId"))
and
#Column(name="discordId", insertable=true, updatable=false)
private String discordId;
As you can see, there is a parameter 'updatable' which is set to false. Therefore, when you are trying to update an already existing object, hibernate is throwing UniqueConstraintViolationException.
To fix this, set 'updatable=true' or remove it altogether and it should work fine.
#Column(name="discordId", insertable=true, updatable=true)
private String discordId;

Spring Data lock table to read while writing

I am using Spring data in my app (2.0.1). The app is REST-based service which add orders.
The order entity looks as follows:
#Entity
#Table(name = "orders")
#Data
#NoArgsConstructor
public class OrderEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Version
private int version;
private Date date;
private String post;
private BigDecimal totalAmount;
#Enumerated(EnumType.STRING)
private OrderStatus status;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "order_id")
List<OrderSetEntity> sets;
private int dailyNumber;
private String documentNumber;
The posting or orders happens in OrderService:
public Long postOrder(OrderDTO orderDTO){
Date date = new Date();
OrderEntity order = new OrderEntity(date, orderDTO.getPost(), orderDTO.getPaymentMethod());
Integer dailyNumber = orderRepository.findDailyNumberByDate(date) + 1;
order.setDailyNumber(dailyNumber);
orderRepository.save(order);
return order.getId();
}
while findDailyNumberByDate is implemented this way:
#Override
public int findDailyNumberByDate(Date date) {
String sql = String.format("select max(o.dailyNumber) from OrderEntity o ");
Query query = entityManager.createQuery(sql);
Integer result = (Integer) query.getSingleResult();
if (result == null){
return 0;
}else {
return result.intValue();
}
Now I have a problem, that it get duplicated dailyNumber. The table isn't locked for reading when I am about to write into it.
How can I achieve it?
I tried lockin the table - #Lock(LockModeType.PESSIMISTIC_WRITE)
or query.setLockMode(LockModeType.PESSIMISTIC_FORCE_INCREMENT);
but is still isn't working.
Thanks a lot for help
LockModeType can work in different ways with different databases, especially with Oracle db it gets a little tricky. A similar issue was answered here jpa lockmode type npt working as expected

How to delete entity from database

i am new on hibernate-spring tirple..
i just try to code simple register book.. i have following codes:
Student.java
#Entity(name = "STUDENTS")
#NamedQueries({
#NamedQuery(name = "getAllStudent", query = "SELECT k FROM STUDENTS k ORDER BY k.id DESC"),
#NamedQuery(name = "findByName", query = "SELECT k FROM STUDENTS k WHERE k.name LIKE :name")
})
public class Student {
#Column(name = "STUDENTNO", nullable = false)
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(name = "NAME", nullable = false)
private String name;
#Column(name = "SURNAME")
private String surname;
#Column(name = "AGE")
private String age;
// GET ve SET metods
StduentDAO.java
#Repository
#Transactional(readOnly = true)
public class StudentDAO implements IStudentDAO {
#PersistenceContext
EntityManager em;
#Override
public void deleteStudent(Student student) {
Student temp = em.getReference(Student.class, student.getId());
em.remove(temp);
System.out.println("### getting out from studentDAO deleteStudent method ###")
StudentController.java
#Component
#Scope(value = "request")
public class StudentController {
#Autowired
IStudentDAO studentDAO;
List<Student> allStudentList = new ArrayList();
Student student = new Student();
#PostConstruct
private void loadStudents() {
allStudentList = studentDAO.allStudent();
public void deleteStudent() {
studentDAO.deleteStudent(student);
System.out.println("### getting out from StudentController deleteStudent method ### ");
}
When I run deleteStudent() codes i am getting:
"### getting out from studentDAO deleteStudent method ###"
"### getting out from StudentController deleteStudent method ### "
i see these on output but nothing is deleting from database.. i searched a bit and i found this "every entitiy manager's methods open own session." that is why it says i should write my StudentDAO's deleteStudent methof like above..
i think i am missing something about transaciton but i have not recognized yet..
what should i do about this ?
Thanks..
#Transactional annotation create a transaction on your DBMS.
If you use (readOnly = true) you prevent operation on your DB (as INSERT/UPDATE/DELETE).
Remove readOnly = true so your delete method will work.

Spring JPATemplate DAO. DELETE Items from table

News
#Entity
#Table(name = "NEWS")
#NamedQueries({
#NamedQuery(name = "News.findAll", query = "SELECT n FROM News n"),
#NamedQuery(name = "News.delete", query = "DELETE FROM News n WHERE n.newsId in(:ids)")
})
#GenericGenerator(name = "test-increment-strategy", strategy = "increment")
public class News implements Serializable {
private static final long serialVersionUID = 3330980835510468207L;
private Integer newsId;
private String title;
private String brief;
private String content;
private Date created;
private String dateCreatedString;
public News() {
}
#Id
#Column(name = "NEWS_ID")
// #GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "news_seq")
// #SequenceGenerator(name = "news_seq", sequenceName = "NEWS_SEQ")
#GeneratedValue(generator = "test-increment-strategy")
public Integer getNewsId() {
return newsId;
}
DAO
#Override
#Transactional(readOnly = false)
public void removeNews(List<Integer> listOfIdNewsForDeleting) throws DAOException {
EntityManager entityManager = getJpaTemplate().getEntityManagerFactory().createEntityManager();
Query query = entityManager.createNamedQuery("News.delete");
query.setParameter("ids", listOfIdNewsForDeleting);
int deleted = query.executeUpdate();
}
It's unsuccessful attempt delete news.
TransactionManager doesn't give transaction for new EntityManager which i call.
But i can't use query throw JpaTemplate. Have some idea?
The transaction manager does not start transactions for manually created entity managers. It only handles container/spring-managed entity managers. And without a transaction you can't delete.
The javadocs of JpaTemplate advise you to use the JPA-style data access. So use
#PersistenceContext
private EntityManager entityManager;
(check the spring docs for what you need to enable it).
If you really want to huse the JpaTemplate then use only it, and don't get the underlying factory.

Resources