I have a basic SpringBoot app. using Spring Initializer, JPA, embedded Tomcat, Thymeleaf template engine, and package as an executable JAR file. I have created this Repository class:
#Repository
public interface MenuRepository extends CrudRepository<Menu, Long> {
#Cacheable("menus")
#Query("select cur from Menu cur where cur.MenuId = ?1")
Menu findMenuByMenuId (String MenuId);
#Cacheable("menus")
#Query("select cur from Menu cur where lower (cur.symbol) = lower (?1) ")
Menu findMenuBySymbol (String symbol);
and this service:
#Service
public class MenuService {
#Autowired
protected MenuRepository menuRepository;
#Autowired
MenuPriceService menuPriceService;
#Transactional
public Menu save (Menu menu) {
return menuRepository.save (menu);
}
#Transactional
public void delete (Menu menu) {
menuRepository.delete (menu);
}
..
}
}
and this JUNIT test:
#Test
#Transactional
public void testDelete () {
String menuName = System.currentTimeMillis()+"";
Menu menu = new Menu();
menu.setMenuId(menuName);
menuService.delete (menu);
}
but this is what I see in the console
Hibernate:
insert
into
t_menu
(...)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
While removing #Transactional as suggested in another answer might make the behavior go away, I'd question the validity of the test.
The question really is: why do you try to delete an entity that is not in the database?
Solution:
If you simulate the case where don't know if the entity exists, use deleteById it will throw an exception when the id is not present in the database and you can, of course, catch that.
Some background to how and why this insert happens:
delete(T entity) is implemented through SimpleJpaRepository.delete(T entity) which basically contains this single line of code.
em.remove(em.contains(entity) ? entity : em.merge(entity));
As you can see it does a merge on the entity if the entity isn't present in the EntityManager. Without the merge you'd might end up with duplicate entities in the session if another instance with same class and Id does exist in the EntityManager. But since your entity doesn't exist at all this triggers an insert into the database.
You might think it as this: You create an instance, you make the EntityManager know about it (by telling it to delete it) and you delete it, so these are the steps that need to occur in the database -> insert + delete.
remove #Transactional:
#Test
public void testDelete () {
String menuName = System.currentTimeMillis()+"";
Menu menu = new Menu();
menu.setMenuId(menuName);
menuService.delete (menu);
}
Related
i have this in my repository
#Transactional
#Modifying
#Query(value = "UPDATE pengajuan u set u.status_approval ='Komite' where u.id_pengajuan =:idPengajuan",
nativeQuery = true)
void updateStatusPengajuan(#Param("statusApproval") String statusApproval, #Param("idPengajuan") Integer idPengajuan);
i want to set status_approval to 'Komite' by 'idPengajuan'
now i have this in my services
public PengajuanK3 update(String statusApproval,int idPengajuan){
return pengajuanK3Repository.updateStatusPengajuan(statusApproval, idPengajuan);
}
im littlebit confuse how i can call the repository in services because in repository is void type.
The query that you have used always sets status_approval to Komite. In this sense, you don't need to pass the parameter in your repository update method. Everything is fine.
But if you want to update status_approval dynamically from the parameter other than 'Komite' then do like this:
Repository:
#Transactional
#Modifying
#Query(value = "UPDATE pengajuan u set u.status_approval =:statusApproval where u.id_pengajuan =:idPengajuan",
nativeQuery = true)
void updateStatusPengajuan(#Param("statusApproval") String statusApproval,
#Param("idPengajuan") Integer idPengajuan);
And in your service change the return type to void and remove return.
public void update(String statusApproval,int idPengajuan){
pengajuanK3Repository.updateStatusPengajuan(statusApproval,idPengajuan);
}
In your controller, call the update method like:
service.update('Komite', 1);
I prefer this way rather than hardcoding because in the future if you need to set status_approval to other values, you can do it by:
service.update('othervalues', 1);
It's possible to user Spring boot Audit with JPA #Query annotation?
For example i have the next Repository:
#Repository
public interface ItemRepository extends JpaRepository<Item, Long> {
#Modifying
#Query(value = "update Item i set statusId = :statusId WHERE i.id = :id")
void updateById(#Param("id") Long id, #Param("statusId") Long statusId);
}
But when i use this into my service it's not updating update_date column in database:
#Autowired
private ItemRepository itemRepository;
#Transactional
public void updateItemStatus(Long itemId, Long statusId) {
//Case 1: This is working, update_date column is updated
Item item = itemRepository.findById(itemId).orElseThrow(() -> new ResourceNotFoundException("Item", "id", itemId));
item.setStatusId(statusId);
itemRepository.save(item);
//Case 2: This is not working
itemRepository.updateById(itemId, statusId);
}
So, can i use Audit with #Query annotation ?
Thank you.
Auditing is based on the JPA Lifecycle events. Only the methods directly manipulating instances (persist, merge and remove) trigger such events.
The execution of queries, modifying or otherwise, does not trigger any events and therefore, won't cause auditing to happen.
You have syntax issue
Either use
#Query(value = "update Item i set i.statusId = :statusId WHERE i.id = :id")
or
#Query(value = "update Item set statusId = :statusId WHERE id = :id")
Is there a better way to update a versioned entity other than to catch exception and retrying ?
Below is the code
#Entity
#AllArgsConstructor
#Builder
#NoArgsConstructor
#Data
#IdClass(com.test.domain.EmployeeKey.class)
public class Employee {
#Id
private Integer employeeId;
private String name;
private Double commission;
#Id
private LocalDate inputDate;
#Version
private Integer version;
}
public class EmployeeKey implements Serializable {
private Integer employeeId;
private java.time.LocalDate inputDate;
}
#SpringBootApplication
#Slf4j
public class SampleApplication {
public static void main(final String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
#Component
public class AppStartupRunner implements ApplicationRunner {
#Autowired
EmployeeRepo employeeRepo;
#Override
public void run(ApplicationArguments args) throws Exception {
log.info("Your application started with option names : {}", args.getOptionNames());
List<Employee> empList = new ArrayList();
Employee employeeComm1 = Employee.builder().employeeId(1).name("Sam").commission(100.45).inputDate(LocalDate.of(2019,10,01)).build();
Employee employeeComm2 = Employee.builder().employeeId(1).name("Sam").commission(87.54).inputDate(LocalDate.of(2019,10,01)).build();
Employee employeeComm3 = Employee.builder().employeeId(2).name("John").commission(56.78).inputDate(LocalDate.of(2019,10,01)).build();
Employee employeeComm4 = Employee.builder().employeeId(3).name("Katie").commission(65.23).inputDate(LocalDate.of(2019,10,01)).build();
empList.add(employeeComm1);
empList.add(employeeComm3);
empList.add(employeeComm4);
empList.add(employeeComm2);
// employeeRepo.saveAll(empList);
for (Employee emp: empList) {
try {
employeeRepo.save(emp);
}
catch(Exception e) {
if (employeeRepo.findByEmployeeIdAndInputDate(emp.getEmployeeId(), emp.getInputDate()).isPresent()) {
Employee empFromDb = employeeRepo.findByEmployeeIdAndInputDate(emp.getEmployeeId(), emp.getInputDate()).get();
empFromDb.setCommission(emp.getCommission());
empFromDb.setName(emp.getName());
employeeRepo.save(empFromDb);
}
}
}
}
}
}
Output shown below
Hibernate: insert into employee (commission, name, version, employee_id, input_date) values (?, ?, ?, ?, ?)
Hibernate: insert into employee (commission, name, version, employee_id, input_date) values (?, ?, ?, ?, ?)
Hibernate: insert into employee (commission, name, version, employee_id, input_date) values (?, ?, ?, ?, ?)
Hibernate: insert into employee (commission, name, version, employee_id, input_date) values (?, ?, ?, ?, ?)
2019-10-15 16:43:10.372 WARN 17216 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1, SQLState: 23000
2019-10-15 16:43:10.372 ERROR 17216 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : ORA-00001: unique constraint (I706446.SYS_C008493) violated
Hibernate: select employee0_.employee_id as employee_id1_0_, employee0_.input_date as input_date2_0_, employee0_.commission as commission3_0_, employee0_.name as name4_0_, employee0_.version as version5_0_ from employee employee0_ where employee0_.employee_id=? and employee0_.input_date=?
Hibernate: select employee0_.employee_id as employee_id1_0_, employee0_.input_date as input_date2_0_, employee0_.commission as commission3_0_, employee0_.name as name4_0_, employee0_.version as version5_0_ from employee employee0_ where employee0_.employee_id=? and employee0_.input_date=?
Hibernate: select employee0_.employee_id as employee_id1_0_0_, employee0_.input_date as input_date2_0_0_, employee0_.commission as commission3_0_0_, employee0_.name as name4_0_0_, employee0_.version as version5_0_0_ from employee employee0_ where employee0_.employee_id=? and employee0_.input_date=?
Hibernate: update employee set commission=?, name=?, version=? where employee_id=? and input_date=? and version=?
I expect an hibernate to issue an update with Version column
incrementing
Your expectation is incorrect. You use version not as it is intended to be used. The purpose of versioning is to provide optimistic locking and not simply indicate how many times has the entity changed. The intended usage scenario is following: before updating an entity you should read it from the database, then modify, then save. Hibernate checks if the version you are trying to save is the same as in the database.
A) If the same, it will automatically increment version and save your entity, i.e. it will update the existing record in the database.
B) If the version in the entity you are trying to save is less than the version in the database, it means you are trying to save an outdated version. It means you are not aware of changes that were done in the meanwhile. In such case Hibernate will throw an exception.
In my Spring boot app I'm deleting and inserting a large amount of data into my MySQL db in a single transaction. Ideally, I want to only commit the results to my database at the end, so all or nothing. I'm running into issues where my deletions will be committed before my insertions, so during that period any calls to the db will return no data (not good). Is there a way to manually commit transaction?
My main logic is:
#Transactional
public void saveParents(List<Parent> parents) {
parentRepo.deleteAllInBatch();
parentRepo.resetAutoIncrement();
//I'm setting the id manually before hand
String sql = "INSERT INTO parent " +
"(id, name, address, number) " +
"VALUES ( ?, ?, ?, ?)";
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
#Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
Parent parent = parents.get(i);
ps.setInt(1, parent.getId());
ps.setString(2, parent.getName());
ps.setString(3, parent.getAddress());
ps.setString(4, parent.getNumber());
}
#Override
public int getBatchSize() {
return parents.size();
}
});
}
ParentRepo
#Repository
#Transactional
public interface ParentRepo extends JpaRepository<Parent, Integer> {
#Modifying
#Query(
value = "alter table parent auto_increment = 1",
nativeQuery = true
)
void resetAutoIncrement();
}
EDIT:
So I changed
parentRepo.deleteAllInBatch();
parentRepo.resetAutoIncrement();
to
jdbcTemplate.update("DELETE FROM output_stream");
jdbcTemplate.update("alter table output_stream auto_increment = 1");
in order to try avoiding jpa's transaction but each operation seems to be committing separately no matter what I try. I have tried TransactionTemplate and implementing PlatformTransactionManager (seen here) but I can't seem to get these operations to commit together.
EDIT: It seems the issue I was having was with the alter table as it will always commit.
I'm running into issues where my deletions will be committed before my insertions, so during that period any calls to the db will return no data
Did you configure JPA and JDBC to share transactions?
If not, then you're basically using two different mechanisms to access the data (EntityManager and JdbcTempate), each of them maintaining a separate connection to the database. What likely happens is that only EntityManager joins the transaction created by #Transactional; the JdbcTemplate operation executes either without a transaction context (read: in AUTOCOMMIT mode) or in a separate transaction altogether.
See this question. It is a little old, but then again, using JPA and Jdbc together is not exactly a common use case. Also, have a look at the Javadoc for JpaTransactionManager.
I am new to Hibernate and I encountered the classic "detached entity passed to persist" exception. I read a few similar questions here but non of them can apply to my situation.
I have 2 entities, Department and DeptEmpCode. Department has a foreign key that references DeptEmpCode. Many Departments may share one code, So it is many to one.
The entity code is as follows:
Department:
#Entity
#Table(name = "department")
public class Department {
private Integer id;
...
private DeptEmpCode status;
...
#ManyToOne(cascade=CascadeType.ALL)
#JoinColumn(name = "status")
public DeptEmpCode getStatus() {
return status;
}
public void setStatus(DeptEmpCode status) {
this.status = status;
}
}
DeptEmpCode:
#Entity
#Table(name = "code")
public class DeptEmpCode {
private Integer id;
....
}
I have omitted unnecessary code for easier read.
So when I hit the "add Department" button on the webpage, spring framework creates a Department object for me. Then I use "new" to create a DeptEmpCode object, and call Department's setStatus() to associate the DeptEmpCode object to the Department. Then I persist the Department. The code is as follows:
public void saveDept(Department dept) {
if(dept.getStatus() == null){
DeptEmpCode status = new DeptEmpCode();
status.setId(Constants.DEFAUL_DEPT_STATUS_ID);
dept.setStatus(status);
}
deptDAO.save(dept); //nothing but a persist() call
}
So what should be the problem? Should I make it bi-directional or its something else?(If I remove "cascadeType.ALL" then it would be a foreign key violation). Thanks!
since your Department object is already created. Use cascadeType.MERGE instead of cascadeType.ALL
When you use cascadeType.ALL, it will think the transaction is PERSISTED, it tries to PERSIST Department as well and that doesn't work since Department already is in the db. But with CascadeType.MERGE the Department is automatically merged(update) instead.
Update:
Service
#Override
#Transactional
public void saveDept(Department dept) {
if(dept.getStatus() == null){
DeptEmpCode status = new DeptEmpCode();
status.setId(Constants.DEFAUL_DEPT_STATUS_ID);
status.setType("DEFAULT");
status.setValue("DEFAULT");
dept.setStatus(status);
}
deptDAO.update(dept);
}
DAO
#Override
public void update(Department dept) {
em.merge(dept);
}
Explanation:
since status.id is a primary key and you set the value by yourself. When you call save it will create the status first, so it means the status has already (detached object) register in the database(not yet commit), if you still use em.persist(dept), it will also try to persist the detach object (status) and the department. so we should use merge which will merge the status and insert a new department.
you can see below is how hibernate insert your record.
however, if you don't set the status.id value and let it auto-generate, then it will persist department and status at the same time. so you won't have the problem. In your case, I know you want to assign the status.id to default which is one. So you should use merge as you will have the 2nd and 3rd department that use the default status id (which status id already in db).
Hibernate: insert into code (DES, INACTIVE_IND, CODE_TYPE, VALUE) values (?, ?, ?, ?)
Hibernate: insert into department (contact, des, dept_email, dept_name, status) values (?, ?, ?, ?, ?)