How does Spring Batch CompositeItemWriter manage transaction for delegate writers? - spring

In the batch job step configuration, I plan to execute 2 queries in the writer, the 1st query is to update records in table A, then the 2nd query is to insert new records in table A again.
So far I think CompositeItemWriter can achieve my goal above, i.e., I need to create 2 JdbcBatchItemWriters, one is for update, and the other one is for insert.
My first question is if CompositeItemWriter is a fit for the requirement above?
If yes, that lead to the second question about transaction. For example, if the first update is successful, and the second insert fails. Will the 1st update transaction be rolled back automatically? Otherwise, how to manually pull both updates in the same transaction?
Thanks in advance!

My first question is if CompositeItemWriter is a fit for the requirement above?
Yes, CompositeItemWriter is the way to go.
If yes, that lead to the second question about transaction. For example, if the first update is successful, and the second insert fails. Will the 1st update transaction be rolled back automatically? Otherwise, how to manually pull both updates in the same transaction?
Excellent question! Yes, if the update succeeds in the first writer and then the insert fails in the second writer, all statements will be rolled back automatically. What you need to know is that the transaction is around the execution of the chunk oriented tasklet step (and so around the write method of the composite item writer). Hence, the execution of all sql statements within this method (executed in delegate writers) will be atomic.
To illustrate this use case, I wrote the following test:
Given a table people with two columns id and name with only one record inside it: 1,'foo'
Let's imagine a job that reads two records (1,'foo', 2,'bar') and tries to update foo to foo!! and then inserts 2,'bar' in the table. This is done with a CompositeItemWriter with two item writers: UpdateItemWriter and InsertItemWriter
The use case is that UpdateItemWriter succeeds but InsertItemWriter fails (by throwing an exception)
The expected result is that foo is not updated to foo!! and bar is not inserted in the table (Both sql statements are rolled back due to the exception in the InsertItemWriter)
Here is the code (it is self-contained so you can try it and see how things work, it uses an embedded hsqldb database which should be in your classpath):
import java.util.Arrays;
import java.util.List;
import javax.sql.DataSource;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.support.CompositeItemWriter;
import org.springframework.batch.item.support.ListItemReader;
import org.springframework.batch.test.JobLauncherTestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.jdbc.JdbcTestUtils;
#RunWith(SpringRunner.class)
#ContextConfiguration(classes = TransactionWithCompositeWriterTest.JobConfiguration.class)
public class TransactionWithCompositeWriterTest {
#Autowired
private JobLauncherTestUtils jobLauncherTestUtils;
#Autowired
private JdbcTemplate jdbcTemplate;
#Before
public void setUp() {
jdbcTemplate.update("CREATE TABLE people (id INT IDENTITY NOT NULL PRIMARY KEY, name VARCHAR(20));");
jdbcTemplate.update("INSERT INTO people (id, name) VALUES (1, 'foo');");
}
#Test
public void testTransactionRollbackWithCompositeWriter() throws Exception {
// given
int peopleCount = JdbcTestUtils.countRowsInTable(jdbcTemplate, "people");
int fooCount = JdbcTestUtils.countRowsInTableWhere(jdbcTemplate, "people", "id = 1 and name = 'foo'");
int barCount = JdbcTestUtils.countRowsInTableWhere(jdbcTemplate, "people", "id = 2 and name = 'bar'");
Assert.assertEquals(1, peopleCount);
Assert.assertEquals(1, fooCount);
Assert.assertEquals(0, barCount);
// when
JobExecution jobExecution = jobLauncherTestUtils.launchJob();
// then
Assert.assertEquals(ExitStatus.FAILED.getExitCode(), jobExecution.getExitStatus().getExitCode());
Assert.assertEquals("Something went wrong!", jobExecution.getAllFailureExceptions().get(0).getMessage());
StepExecution stepExecution = jobExecution.getStepExecutions().iterator().next();
Assert.assertEquals(0, stepExecution.getCommitCount());
Assert.assertEquals(1, stepExecution.getRollbackCount());
Assert.assertEquals(0, stepExecution.getWriteCount());
peopleCount = JdbcTestUtils.countRowsInTable(jdbcTemplate, "people");
fooCount = JdbcTestUtils.countRowsInTableWhere(jdbcTemplate, "people", "id = 1 and name = 'foo'");
barCount = JdbcTestUtils.countRowsInTableWhere(jdbcTemplate, "people", "id = 2 and name = 'bar'");
Assert.assertEquals(1, peopleCount); // bar is not inserted
Assert.assertEquals(0, barCount); // bar is not inserted
Assert.assertEquals(1, fooCount); // foo is not updated to "foo!!"
}
#Configuration
#EnableBatchProcessing
public static class JobConfiguration {
#Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("/org/springframework/batch/core/schema-drop-hsqldb.sql")
.addScript("/org/springframework/batch/core/schema-hsqldb.sql")
.build();
}
#Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
#Bean
public ItemReader<Person> itemReader() {
Person foo = new Person(1, "foo");
Person bar = new Person(2, "bar");
return new ListItemReader<>(Arrays.asList(foo, bar));
}
#Bean
public ItemWriter<Person> updateItemWriter() {
return new UpdateItemWriter(dataSource());
}
#Bean
public ItemWriter<Person> insertItemWriter() {
return new InsertItemWriter(dataSource());
}
#Bean
public ItemWriter<Person> itemWriter() {
CompositeItemWriter<Person> compositeItemWriter = new CompositeItemWriter<>();
compositeItemWriter.setDelegates(Arrays.asList(updateItemWriter(), insertItemWriter()));
return compositeItemWriter;
}
#Bean
public Job job(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory) {
return jobBuilderFactory.get("job")
.start(stepBuilderFactory
.get("step").<Person, Person>chunk(2)
.reader(itemReader())
.writer(itemWriter())
.build())
.build();
}
#Bean
public JobLauncherTestUtils jobLauncherTestUtils() {
return new JobLauncherTestUtils();
}
}
public static class UpdateItemWriter implements ItemWriter<Person> {
private JdbcTemplate jdbcTemplate;
public UpdateItemWriter(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
#Override
public void write(List<? extends Person> items) {
for (Person person : items) {
if ("foo".equalsIgnoreCase(person.getName())) {
jdbcTemplate.update("UPDATE people SET name = 'foo!!' WHERE id = 1");
}
}
}
}
public static class InsertItemWriter implements ItemWriter<Person> {
private JdbcTemplate jdbcTemplate;
public InsertItemWriter(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
#Override
public void write(List<? extends Person> items) {
for (Person person : items) {
if ("bar".equalsIgnoreCase(person.getName())) {
jdbcTemplate.update("INSERT INTO people (id, name) VALUES (?, ?)", person.getId(), person.getName());
throw new IllegalStateException("Something went wrong!");
}
}
}
}
public static class Person {
private long id;
private String name;
public Person() {
}
public Person(long id, String name) {
this.id = id;
this.name = name;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
My example uses custom item writers but this should work with two JdbcBatchItemWriters as well.
I hope this helps!

Related

Create a #JobScope #Service with Spring Batch that holds shared memory for a job?

Essentially what i'm looking to do is create a #Service or component that loads some data into memory from a database table which is referenced throughout the job execution
package com.squareup.se.bridge.batchworker.components.context;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import com.squareup.se.bridge.batchworker.repositories.BridgeBatchJobParametersRepository;
import com.squareup.se.bridge.batchworker.util.JobParameterKeys;
import com.squareup.se.bridge.core.api.services.batchworker.FatalSyncException;
import com.squareup.se.bridge.core.integration.util.logger.JobExecutionLoggerFactory;
import java.io.IOException;
import javax.validation.constraints.NotNull;
import org.slf4j.Logger;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionListener;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.stereotype.Component;
#JobScope #Component public class BridgeBatchIntegrationJobContextProvider
implements JobExecutionListener {
private Logger logger;
private ObjectMapper mapper;
private BridgeBatchJobParametersRepository bridgeBatchJobParametersRepository;
private BridgeIntegrationJobContext context;
public BridgeBatchIntegrationJobContextProvider(ObjectMapper mapper,
BridgeBatchJobParametersRepository bridgeBatchJobParametersRepository) {
this.mapper = mapper;
this.bridgeBatchJobParametersRepository = bridgeBatchJobParametersRepository;
}
#Override public void beforeJob(JobExecution jobExecution) {
var jobId = jobExecution.getJobParameters().getString(JobParameterKeys.SYNC_ID);
this.logger = JobExecutionLoggerFactory.getLogger(
BridgeBatchIntegrationJobContextProvider.class, jobId);
this.context = deserializeJobParameters(jobId);
}
#NotNull public BridgeIntegrationJobContext get() {
if (context == null) {
throw new IllegalStateException("Expected context to exist before calling this method");
}
return context;
}
#Override public void afterJob(JobExecution jobExecution) { }
#NotNull private String getParameters(String jobId) {
var jobParams = bridgeBatchJobParametersRepository.find(jobId);
if (jobParams == null || jobParams.size() == 0) {
throw new FatalSyncException(String.format("No job parameters for job `%s` exists", jobId));
}
if (jobParams.size() > 1) {
throw new FatalSyncException(String.format("Multiple parameter entries exist for job `%s`",
jobId));
} else if (Strings.isNullOrEmpty(jobParams.get(0).getIntegrationContext())) {
throw new FatalSyncException(String.format("Job parameters for job `%s` is empty", jobId));
}
return jobParams.get(0).getIntegrationContext();
}
#NotNull private BridgeIntegrationJobContext deserializeJobParameters(String jobId) {
try {
return mapper.readValue(getParameters(jobId),
BridgeIntegrationJobContext.class);
} catch (IOException e) {
//TODO page on this
logger.info(e.getMessage(), e);
throw new FatalSyncException(e);
}
}
}
I've configured a job like this:
return jobBuilderFactory.get(CUSTOMERS_BATCH_JOB_NAME)
.incrementer(new RunIdIncrementer())
.start(loadFromOriginStep)
.next(retryFailuresFromOriginStep)
.listener(bridgeBatchIntegrationJobContextProvider)
.listener(jobListener)
.build();
The constructor depends on other beans including a jackson object mapper and a JPA repo. I'm encountering a few problems:
the constructor is not instantiated by Spring and thus the instance
variables I want to bind are not present
If I remove #JobScope from the component, Spring constructs the component instance.
I don't see where #JobContext is used in your code, and according to your requirement, you don't need it.
If you want to load some data in the job execution context using a listener, you can do it in beforeJob with jobExecution.getExecutionContext().put("key", "value");.
That said, it is not recommended to load a lot of data in the execution context as it is persisted between steps.
So unless you are loading a small amount of data in the execution context, you need to find another approach (like using a separate cache for example, see Spring Batch With Annotation and Caching).

How to put a lock and transaction on table using spring 4 or above using jdbcTemplate and annotations like #Transactional?

I am trying to put lock on table while writing on table and if something happened in between then roll-back .
Trying to convert below code
lock table test_g1 read;
lock table test_g write;
-- BEGIN;
START TRANSACTION;
insert into test_g1 values(143);
insert into test_g values(145);
select * from test_g1;
select * from test_g;
Rollback;
select * from test_g;
unlock tables;
How to convert above code into #Transactional spring jdbcTemplate code?
#Transactional(rollbackFor={DataAccessException.class})
public void Test(){
jdbcTemplate.execute("insert into test1 (id, nam) values (4, 'A')");
throw new DataAccessException("error") {
};
}
Here i am trying to throw error, so insert statement should rollback but it's not happening .
Thanks
EDIT-1
I am attaching the code , what exactly i am doing
In JdbcDaoImpl.java , i mentioned my problem as a comment above Test() .
App.java
package com.cgiri.javabrains.Spring4;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.transaction.annotation.Transactional;
public class App
{
public static void main( String[] args )
{
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
App2 app2 = ctx.getBean("app2",App2.class);
app2.call();
}
}
App2.java
package com.cgiri.javabrains.Spring4;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
#Component
public class App2 {
#Autowired
private ApplicationContext ctx = null;
JdbcDaoImpl jdbcDaoImpl ;
public void call( )
{
jdbcDaoImpl = ctx.getBean("jdbcDaoImpl",JdbcDaoImpl.class);
System.out.println(jdbcDaoImpl.getCount());
try{
jdbcDaoImpl.Test();
}catch(Exception e)
{
}
System.out.println(jdbcDaoImpl.getCount());
}
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.ctx = context;
}
}
JdbcDaoImpl.java
package com.cgiri.javabrains.Spring4;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
#Component
#Transactional
public class JdbcDaoImpl {
private JdbcTemplate jdbcTemplate;
private DataSource dataSource;
public DataSource getDataSource() {
return dataSource;
}
#Autowired
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int getCount() {
String sql = "SELECT COUNT(*) FROM test1";
// jdbcTemplate.setDataSource(getDataSource());
return jdbcTemplate.queryForObject(sql, Integer.class);
}
public void crateTable() {
String sql = "create table if not exists test1 (id integer, nam char(50))";
jdbcTemplate.execute(sql);
jdbcTemplate.execute("insert into test1 (id, nam) values (1, 'A')");
int count = jdbcTemplate.queryForObject("select count(*) from test1",Integer.class);
System.out.println(count);
}
/**** This is the point where i am trying to rollback insert query ,but it's not happening , instead it's inserting the data into the table and just throwing exception , rollback is not happeneing ****/
#Transactional(rollbackFor={DataAccessException.class})
public void Test(){
jdbcTemplate.execute("insert into test1 (id, nam) values (4, 'A')");
throw new DataAccessException("error") {
};
}
}
AppConfig.java
package com.cgiri.javabrains.Spring4;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcCall;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionTemplate;
#Configuration
#ComponentScan({ "com.cgiri.javabrains.Spring4" })
#PropertySource("classpath:db.properties")
public class AppConfig {
private JdbcTemplate jdbcTemplate;
private TransactionTemplate transactionTemplate;
#Autowired
private Environment env;
#Bean
public BasicDataSource getBasicDataSource()
{
BasicDataSource dao = new BasicDataSource();
dao.setDriverClassName(env.getProperty("db.driverClassName"));
dao.setUrl(env.getProperty("db.url"));
dao.setUsername(env.getProperty("db.userName"));
dao.setPassword(env.getProperty("db.password"));
dao.setInitialSize(2);
dao.setMaxActive(5);
return dao;
}
#Bean
public DataSourceTransactionManager getTransactionManager(BasicDataSource dataSource) {
DataSourceTransactionManager manager = new DataSourceTransactionManager(dataSource);
return manager;
}
#Autowired
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
}
And for locking the table during DML query, if 2 or more people are updating simultaneously on table , this locking mechanism is taken care by mysql server or we have to configure separately for that the way we are doing for transactions ?
Thanks
If you don't want to use #Transactional then you could try to use TransactionTemplate and something like this:
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
#Component
public SimpleDao {
#Autowired
private JdbcTemplate jdbcTemplate;
#Autowired
private TransactionTemplate transactionTemplate;
private void executeTransactionWithoutResult(DbTransactionTask dbTask) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
#Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
dbTask.executeTask();
}
});
}
public void test() {
DbTransactionTask dbTask = new DbTransactionTask() {
#Override
public void executeTask() {
try {
jdbcTemplate.execute("LOCK TABLES Entry WRITE;");
jdbcTemplate.execute("...");
jdbcTemplate.execute("...");
jdbcTemplate.execute("UNLOCK TABLES;")
} catch (Exception e) {
// Cause rollback of transaction
throw new RuntimeException("Reverting DB operations: " + e.getClass().getSimpleName() + " - " + e.getMessage(), e);
}
}
};
executeTransactionWithoutResult(dbTask);
}
abstract class DbTransactionTask { public abstract void executeTask(); }
}
EDIT: With #Transactional you could try something like this:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.transaction.annotation.Transactional;
import dao.EntryDao;
#SpringBootApplication
public class SpringTransactional {
private ConfigurableApplicationContext springContext;
#Autowired
private EntryDao dao;
public void init() {
springContext = SpringApplication.run(SpringTransactional.class);
springContext.getAutowireCapableBeanFactory().autowireBean(this);
}
public static void main(String[] args) {
SpringTransactional st = new SpringTransactional();
try {
st.init();
dao.db_transaction_test();
} catch (RuntimeException e) {
e.printStackTrace();
} finally {
st.springContext.close();
}
}
}
Where EntryDao is:
package dao;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* DB creation and schema:
* CREATE DATABASE db_name;
* CREATE USER db_username;
* <p>
* USE db_name;
* GRANT ALL ON db_name.* TO db_username;
* <p>
* SET PASSWORD FOR spz = PASSWORD('username123');
* FLUSH PRIVILEGES;
* <p>
* CREATE TABLE Entry (
* entry_ID INT NOT NULL AUTO_INCREMENT,
* name TEXT NOT NULL,
* <p>
* PRIMARY KEY (entry_ID)
* );
*/
#Component
public class EntryDao {
/**
* application.properties:
* spring.datasource.driver-class-name = com.mysql.jdbc.Driver
* spring.datasource.url = jdbc:mysql://localhost:3306/db_name?useSSL=false&serverTimezone=UTC
* spring.datasource.username = db_username
* spring.datasource.password = username123
*/
#Autowired
private JdbcTemplate jdbcTemplate;
#Transactional
public void db_transaction_test() {
jdbcTemplate.execute("LOCK TABLES Entry WRITE;");
for (int i = 0; i < 10; i++) {
try {
int entry_name = getEntryId("entry_" + i);
System.out.println("Created entry id=" + entry_name);
} catch (EntryDao.DaoException e) {
e.printStackTrace();
}
if (i == 5) {
throw new RuntimeException("Testing data upload procedure break.");
}
}
jdbcTemplate.execute("UNLOCK TABLES;")
}
public int getEntryId(String entryName) throws DaoException {
List<DbEntry> dbEntries = retrieveEntriesFor(entryName);
if (dbEntries.size() == 1) {
return dbEntries.get(0).getEntry_ID();
} else if (dbEntries.size() == 0) {
String sqlInsert = "INSERT INTO Entry (name) VALUES (?)";
jdbcTemplate.update(sqlInsert, entryName);
dbEntries = retrieveEntriesFor(entryName);
if (dbEntries.size() == 1) {
return dbEntries.get(0).getEntry_ID();
} else {
throw new DaoException("Invalid results amount received after creating new (" + dbEntries.size() + ") when getting entry for name: " + entryName);
}
} else {
throw new DaoException("Invalid results amount received (" + dbEntries.size() + ") when getting entry for name: " + entryName);
}
}
private List<DbEntry> retrieveEntriesFor(String entryName) {
return jdbcTemplate.query("SELECT * FROM Entry WHERE name=?;", (ResultSet result, int rowNum) -> unMarshal(result), entryName);
}
private DbEntry unMarshal(ResultSet result) throws SQLException {
DbEntry dbEntry = new DbEntry();
dbEntry.setEntry_ID(result.getInt("entry_ID"));
dbEntry.setName(result.getString("name"));
return dbEntry;
}
public class DbEntry {
private int entry_ID;
private String name;
int getEntry_ID() { return entry_ID; }
void setEntry_ID(int entry_ID) { this.entry_ID = entry_ID; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
public class DaoException extends Throwable { DaoException(String err_msg) { super(err_msg); } }
}

Entity not getting saved in EclipseLink EntityListener

I have written EntityListener using eclipseLink's "DescriptorEventAdapter". I tried almost all variations whatever present online BUT the entity which I am saving from my listener is not getting saved. I suspect something fishy is going on with transaction but didn't get the root cause. Here is the code :
package com.db;
import java.util.Date;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManagerFactory;
import javax.transaction.Transactional;
import javax.transaction.Transactional.TxType;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.DescriptorEvent;
import org.eclipse.persistence.descriptors.DescriptorEventAdapter;
import org.eclipse.persistence.jpa.JpaEntityManager;
import org.eclipse.persistence.queries.InsertObjectQuery;
import org.eclipse.persistence.queries.UpdateObjectQuery;
import org.eclipse.persistence.sessions.changesets.DirectToFieldChangeRecord;
import org.eclipse.persistence.sessions.changesets.ObjectChangeSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
#Component
public class NotificationMessageListener extends DescriptorEventAdapter {
public static Logger logger = LoggerFactory.getLogger(NotificationMessageListener.class);
private static final String targetColumn = "STATUS";
//private static AuditRepository auditRepo;
#Autowired
private StatusAuditRepository statusAuditRepo;
#Autowired
private RuleResultAuditRepository ruleResultRepo;
#Autowired
private EntityManagerFactory factory;
JpaEntityManager entityManager = null;
#PostConstruct
public void init() {
try {
entityManager = (JpaEntityManager) factory.createEntityManager();
// Use the entity manager to get a ClassDescriptor for the Entity class
ClassDescriptor desc =
entityManager.getSession().getClassDescriptor(NotificationMessage.class);
// Add this class as a listener to the class descriptor
desc.getEventManager().addListener(this);
} finally {
if (entityManager != null) {
// Cleanup the entity manager
entityManager.close();
}
}
}
/*#Autowired
public void setAuditRepo(AuditRepository auditRepo) {
NotificationMessageListener.auditRepo = auditRepo;
}*/
#Transactional(value = TxType.REQUIRES_NEW)
#Override
public void postInsert(DescriptorEvent event) {
logger.info("post insert is called ");
//NotificationMessage notificationMsg = (NotificationMessage) ((InsertObjectQuery) event.getQuery()).getObject();
//entityManager.getTransaction().begin();
NotificationStatusAudit statusAudit = new NotificationStatusAudit();
statusAudit.setInsertionTime(new Date());
//statusAudit.setNewVal(notificationMsg.getStatus());
statusAudit.setNewVal("abc");
statusAudit.setOldval("asdf");
statusAudit.setTargetColumnName("from listner");
//statusAudit.setTargetRecordId(notificationMsg.getId());
statusAudit.setTargetRecordId(123L);
statusAudit = statusAuditRepo.save(statusAudit);
//entityManager.getTransaction().commit();
//logger.info("Number of records "+statusAuditRepo.count());
//auditRuleResult(notificationMsg.getMessageCorrelationId() , true);
}
#Override
public void postUpdate(DescriptorEvent event) {
ObjectChangeSet objectChanges = ((UpdateObjectQuery) event.getQuery()).getObjectChangeSet();
DirectToFieldChangeRecord statusChanges = (DirectToFieldChangeRecord) objectChanges
.getChangesForAttributeNamed("status");
if (statusChanges != null && !statusChanges.getNewValue().equals(statusChanges.getOldValue())) {
NotificationStatusAudit statusAudit = new NotificationStatusAudit();
statusAudit.setInsertionTime(new Date());
statusAudit.setNewVal("abc");
statusAudit.setOldval("asdf");
statusAudit.setTargetColumnName(targetColumn);
statusAudit.setTargetRecordId((Long) objectChanges.getId());
statusAudit = statusAuditRepo.save(statusAudit);
}
}
}
Here all I have to do is save the record in another (Audit) table when data is getting inserted in one table. My application is spring boot app and am using eclipseLink for persistent. I had to manually register my entity-listener in "PostConstruct" because if it is registered using #EntityListner annotation , spring-data-repos were not getting autowired. Here are my questions :
1) Using EntityListener for my requirement is good approach or should I use direct "save" operations ?
2) I debugged the EntityListener code and method is not initiated a new Transaction even after adding Requires_new. I can see method is not being called $proxy (spring-proxy). I don't understand why ?
I am not sure about what you are doing in your #PostConstruct init() method... but I suspect you should be configuring this DescriptorEventAdapter using EclipseLink's DescriptorCustomizer. Here is an example:
public class MessageEventListener extends DescriptorEventAdapter implements DescriptorCustomizer {
#Override
public void customize(ClassDescriptor descriptor) {
descriptor.getEventManager().addListener(this);
}
#Override
public void postUpdate(DescriptorEvent event) {
ObjectChangeSet objectChanges = ((UpdateObjectQuery) event.getQuery()).getObjectChangeSet();
//More business logic...
}
}
#Entity
#Customizer(MessageEventListener.class)
public class Message {
#Id private long id;
private String content;
}

Not supported for DML operations .Unable to update data in postgresql database using spring data

Hi I am using spring boot and Spring data i want to fetch data from database on the basis of id but m not able to retreive it.
M getting this error "exception":
"org.springframework.dao.InvalidDataAccessApiUsageException",
"message": "org.hibernate.hql.internal.QueryExecutionRequestException:
Not supported for DML operations [Update
com.ge.health.poc.model.SpringModel SET name='sneha' where id=?];
nested exception is java.lang.IllegalStateException:
org.hibernate.hql.internal.QueryExecutionRequestException: Not
supported for DML operations [Update
com.ge.health.poc.model.SpringModel SET name='sneha' where id=?]",
"path": "/updatedata"
}
Main Class
package com.ge.health.poc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class SpringDataApplication {
public static void main(String[] args) {
SpringApplication.run(SpringDataApplication.class, args);
}
}
Controller Class
package com.ge.health.poc.controller;
import java.io.IOException;
import java.text.ParseException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ge.health.poc.model.SpringModel;
import com.ge.health.poc.service.BookServiceImpl;
#RestController
public class SpringController {
#Autowired
BookServiceImpl bookserviceimpl;
#RequestMapping(value = "/insertdata", method = RequestMethod.POST)
#ResponseBody
public void helloService(#RequestBody String input, final RedirectAttributes redirectAttributes)
throws JsonParseException, JsonMappingException, IOException, ParseException {
System.out.println(input);
ObjectMapper mapper = new ObjectMapper();
SpringModel pojodata = mapper.readValue(input, SpringModel.class);
System.out.println(pojodata);
System.out.println(pojodata.getAuthor());
bookserviceimpl.save(pojodata);
}
#RequestMapping(value = "/getdata/{id}")
#ResponseBody
public void retreiveData(#PathVariable("id") int id)
throws JsonParseException, JsonMappingException, IOException, ParseException {
System.out.println("id is:" + id);
bookserviceimpl.retreive(id);
}
#RequestMapping(value = "/deletedata", method = RequestMethod.DELETE)
#ResponseBody
public void deleteData(#RequestBody String id)
throws JsonParseException, JsonMappingException, IOException, ParseException {
System.out.println("M in delete");
System.out.println(id);
ObjectMapper mapper = new ObjectMapper();
SpringModel pojodata = mapper.readValue(id, SpringModel.class);
int idd = (pojodata.getId());
System.out.println("value oof idd is:" + idd);
System.out.println("M into delete method");
bookserviceimpl.delete(idd);
}
#RequestMapping(value = "/updatedata", method = RequestMethod.PUT)
#ResponseBody
public void updateData(#RequestBody String id)
throws JsonParseException, JsonMappingException, IOException, ParseException {
System.out.println("M in update");
System.out.println(id);
ObjectMapper mapper = new ObjectMapper();
SpringModel pojodata = mapper.readValue(id, SpringModel.class);
int idd = (pojodata.getId());
System.out.println("value oof idd is:" + idd);
bookserviceimpl.update(idd);
}
}
Repository
package com.ge.health.poc.interfac;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import com.ge.health.poc.model.SpringModel;
#Repository
#Transactional
public interface BookRepository extends JpaRepository<SpringModel, Long> {
#Query("select author from SpringModel where id=?")
String findName(int id);
#Query("Update SpringModel SET name='sneha' where id=?")
String UpdateByID(int id);
#Query("delete from SpringModel where id=?")
String deleteById(int id);
}
BookServiceImpl.java
package com.ge.health.poc.service;
import javax.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.ge.health.poc.interfac.BookRepository;
import com.ge.health.poc.model.SpringModel;
#Component
public class BookServiceImpl implements BookService {
#Autowired
EntityManager entitymanager;
#Autowired
BookRepository bookrepo;
#Override
public void save(SpringModel bookdata) {
bookrepo.save(bookdata);
}
public String retreive(int id) {
String s = bookrepo.findName(id);
System.out.println("Author name is:" + s);
return null;
}
public void delete(int id) {
System.out.println("M into service delete method");
bookrepo.deleteById(id);
}
public void update(int id) {
System.out.println("M in service update");
bookrepo.UpdateByID(id);
}
}
this is model class
package com.ge.health.poc.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
#Entity
#Table(name = "spring_model")
public class SpringModel {
#Id
private Long id;
#Column
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
#Column
private String isbn;
#Override
public String toString() {
return "SpringModel [id=" + id + ", name=" + name + ", isbn=" + isbn + ", author=" + author + ", pages=" + pages
+ "]";
}
#Column
private String author;
#Column
private String pages;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getPages() {
return pages;
}
public void setPages(String pages) {
this.pages = pages;
}
}
Try the annotation #Modifying(org.springframework.data.jpa.repository.Modifying) on the repository methods and #Transactional(org.springframework.transaction.annotation.Transactional) in service implementation which does DML operation. please refer this answer for more information.
By Default spring jpa will think query is select query.So, To make sure the query is updating the existed row for a particular entity
add #modifying Annotation on the method which is updating the existed row
This might works for you

Customizing HATEOAS link generation for entities with composite ids

I have configured a RepositoryRestResource on a PageAndSortingRepository that accesses an Entity that includes a composite Id:
#Entity
#IdClass(CustomerId.class)
public class Customer {
#Id BigInteger id;
#Id int startVersion;
...
}
public class CustomerId {
BigInteger id;
int startVersion;
...
}
#RepositoryRestResource(collectionResourceRel = "customers", path = "customers", itemResourceRel = "customers/{id}_{startVersion}")
public interface CustomerRepository extends PagingAndSortingRepository<Customer, CustomerId> {}
When i access the server at "http://<server>/api/customers/1_1" for instance, I get the correct resource back as json, but the href in the _links section for self is the wrong and also the same for any other customer i query: "http://<server>/api/customer/1"
i.e.:
{
"id" : 1,
"startVersion" : 1,
...
"firstname" : "BOB",
"_links" : {
"self" : {
"href" : "http://localhost:9081/reps/api/reps/1" <-- This should be /1_1
}
}
}
I suppose this is because of my composite Id, But I am chuffed as to how i can change this default behaviour.
I've had a look at the ResourceSupport and the ResourceProcessor class but am not sure how much i need to change in order fix this issue.
Can someone who knows spring lend me a hand?
Unfortunately, all Spring Data JPA/Rest versions up to 2.1.0.RELEASE are not able to serve your need out of the box.
The source is buried inside Spring Data Commons/JPA itself. Spring Data JPA supports only Id and EmbeddedId as identifier.
Excerpt JpaPersistentPropertyImpl:
static {
// [...]
annotations = new HashSet<Class<? extends Annotation>>();
annotations.add(Id.class);
annotations.add(EmbeddedId.class);
ID_ANNOTATIONS = annotations;
}
Spring Data Commons doesn't support the notion of combined properties. It treats every property of a class independently from each other.
Of course, you can hack Spring Data Rest. But this is cumbersome, doesn't solve the problem at its heart and reduces the flexibility of the framework.
Here's the hack. This should give you an idea how to tackle your problem.
In your configuration override repositoryExporterHandlerAdapter and return a CustomPersistentEntityResourceAssemblerArgumentResolver.
Additionally, override backendIdConverterRegistry and add CustomBackendIdConverter to the list of known id converter:
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.rest.core.projection.ProxyProjectionFactory;
import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.data.rest.webmvc.support.HttpMethodHandlerMethodArgumentResolver;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.plugin.core.OrderAwarePluginRegistry;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
#Configuration
#Import(RepositoryRestMvcConfiguration.class)
#EnableSpringDataWebSupport
public class RestConfig extends RepositoryRestMvcConfiguration {
#Autowired(required = false) List<ResourceProcessor<?>> resourceProcessors = Collections.emptyList();
#Autowired
ListableBeanFactory beanFactory;
#Override
#Bean
public PluginRegistry<BackendIdConverter, Class<?>> backendIdConverterRegistry() {
List<BackendIdConverter> converters = new ArrayList<BackendIdConverter>(3);
converters.add(new CustomBackendIdConverter());
converters.add(BackendIdConverter.DefaultIdConverter.INSTANCE);
return OrderAwarePluginRegistry.create(converters);
}
#Bean
public RequestMappingHandlerAdapter repositoryExporterHandlerAdapter() {
List<HttpMessageConverter<?>> messageConverters = defaultMessageConverters();
configureHttpMessageConverters(messageConverters);
RepositoryRestHandlerAdapter handlerAdapter = new RepositoryRestHandlerAdapter(defaultMethodArgumentResolvers(),
resourceProcessors);
handlerAdapter.setMessageConverters(messageConverters);
return handlerAdapter;
}
private List<HandlerMethodArgumentResolver> defaultMethodArgumentResolvers()
{
CustomPersistentEntityResourceAssemblerArgumentResolver peraResolver = new CustomPersistentEntityResourceAssemblerArgumentResolver(
repositories(), entityLinks(), config().projectionConfiguration(), new ProxyProjectionFactory(beanFactory));
return Arrays.asList(pageableResolver(), sortResolver(), serverHttpRequestMethodArgumentResolver(),
repoRequestArgumentResolver(), persistentEntityArgumentResolver(),
resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE,
peraResolver, backendIdHandlerMethodArgumentResolver());
}
}
Create CustomBackendIdConverter. This class is responsible for rendering your custom entity ids:
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import java.io.Serializable;
public class CustomBackendIdConverter implements BackendIdConverter {
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
return id;
}
#Override
public String toRequestId(Serializable id, Class<?> entityType) {
if(entityType.equals(Customer.class)) {
Customer c = (Customer) id;
return c.getId() + "_" +c.getStartVersion();
}
return id.toString();
}
#Override
public boolean supports(Class<?> delimiter) {
return true;
}
}
CustomPersistentEntityResourceAssemblerArgumentResolver in turn should return a CustomPersistentEntityResourceAssembler:
import org.springframework.core.MethodParameter;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.projection.ProjectionDefinitions;
import org.springframework.data.rest.core.projection.ProjectionFactory;
import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler;
import org.springframework.data.rest.webmvc.config.PersistentEntityResourceAssemblerArgumentResolver;
import org.springframework.data.rest.webmvc.support.PersistentEntityProjector;
import org.springframework.hateoas.EntityLinks;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
public class CustomPersistentEntityResourceAssemblerArgumentResolver extends PersistentEntityResourceAssemblerArgumentResolver {
private final Repositories repositories;
private final EntityLinks entityLinks;
private final ProjectionDefinitions projectionDefinitions;
private final ProjectionFactory projectionFactory;
public CustomPersistentEntityResourceAssemblerArgumentResolver(Repositories repositories, EntityLinks entityLinks,
ProjectionDefinitions projectionDefinitions, ProjectionFactory projectionFactory) {
super(repositories, entityLinks,projectionDefinitions,projectionFactory);
this.repositories = repositories;
this.entityLinks = entityLinks;
this.projectionDefinitions = projectionDefinitions;
this.projectionFactory = projectionFactory;
}
public boolean supportsParameter(MethodParameter parameter) {
return PersistentEntityResourceAssembler.class.isAssignableFrom(parameter.getParameterType());
}
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String projectionParameter = webRequest.getParameter(projectionDefinitions.getParameterName());
PersistentEntityProjector projector = new PersistentEntityProjector(projectionDefinitions, projectionFactory,
projectionParameter);
return new CustomPersistentEntityResourceAssembler(repositories, entityLinks, projector);
}
}
CustomPersistentEntityResourceAssembler needs to override getSelfLinkFor. As you can see entity.getIdProperty() return either id or startVersion property of your Customer class which in turn gets used to retrieve the real value with the help of a BeanWrapper. Here we are short circuit the whole framework with the use of instanceof operator. Hence your Customer class should implement Serializable for further processing.
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.model.BeanWrapper;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler;
import org.springframework.data.rest.webmvc.support.Projector;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.Link;
import org.springframework.util.Assert;
public class CustomPersistentEntityResourceAssembler extends PersistentEntityResourceAssembler {
private final Repositories repositories;
private final EntityLinks entityLinks;
public CustomPersistentEntityResourceAssembler(Repositories repositories, EntityLinks entityLinks, Projector projector) {
super(repositories, entityLinks, projector);
this.repositories = repositories;
this.entityLinks = entityLinks;
}
public Link getSelfLinkFor(Object instance) {
Assert.notNull(instance, "Domain object must not be null!");
Class<? extends Object> instanceType = instance.getClass();
PersistentEntity<?, ?> entity = repositories.getPersistentEntity(instanceType);
if (entity == null) {
throw new IllegalArgumentException(String.format("Cannot create self link for %s! No persistent entity found!",
instanceType));
}
Object id;
//this is a hack for demonstration purpose. don't do this at home!
if(instance instanceof Customer) {
id = instance;
} else {
BeanWrapper<Object> wrapper = BeanWrapper.create(instance, null);
id = wrapper.getProperty(entity.getIdProperty());
}
Link resourceLink = entityLinks.linkToSingleResource(entity.getType(), id);
return new Link(resourceLink.getHref(), Link.REL_SELF);
}
}
That's it! You should see this URIs:
{
"_embedded" : {
"customers" : [ {
"name" : "test",
"_links" : {
"self" : {
"href" : "http://localhost:8080/demo/customers/1_1"
}
}
} ]
}
}
Imho, if you are working on a green field project I would suggest to ditch IdClass entirely and go with technical simple ids based on Long class. This was tested with Spring Data Rest 2.1.0.RELEASE, Spring data JPA 1.6.0.RELEASE and Spring Framework 4.0.3.RELEASE.
Although not desirable, I have worked around this issue by using an #EmbeddedId instead of a IdClass annotation on my JPA entity.
Like so:
#Entity
public class Customer {
#EmbeddedId
private CustomerId id;
...
}
public class CustomerId {
#Column(...)
BigInteger key;
#Column(...)
int startVersion;
...
}
I now see the correctly generated links 1_1 on my returned entities.
If anyone can still direct me to a solution that does not require I change the representation of my model, It would be highly appreciated. Luckily I had not progressed far in my application development for this to be of serious concern in changing, but I imagine that for others, there would be significant overhead in performing a change like this: (e.g. changing all queries that reference this model in JPQL queries).
I had a similar problem where the composite key scenarios for data rest was not working. #ksokol detailed explanation provided the necessary inputs to solve the issue. changed my pom primarily for data-rest-webmvc and data-jpa as
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-webmvc</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.7.1.RELEASE</version>
</dependency>
which solved all the issues related to composite key and I need not do the customization. Thanks ksokol for the detailed explanation.
First, create a SpringUtil to get bean from spring.
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
#Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
#Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if(SpringUtil.applicationContext == null) {
SpringUtil.applicationContext = applicationContext;
}
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
public static Object getBean(String name){
return getApplicationContext().getBean(name);
}
public static <T> T getBean(Class<T> clazz){
return getApplicationContext().getBean(clazz);
}
public static <T> T getBean(String name,Class<T> clazz){
return getApplicationContext().getBean(name, clazz);
}
}
Then, implement BackendIdConverter.
import com.alibaba.fastjson.JSON;
import com.example.SpringUtil;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.stereotype.Component;
import javax.persistence.EmbeddedId;
import javax.persistence.Id;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.net.URLEncoder;
#Component
public class CustomBackendIdConverter implements BackendIdConverter {
#Override
public boolean supports(Class<?> delimiter) {
return true;
}
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
if (id == null) {
return null;
}
//first decode url string
if (!id.contains(" ") && id.toUpperCase().contains("%7B")) {
try {
id = URLDecoder.decode(id, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
//deserialize json string to ID object
Object idObject = null;
for (Method method : entityType.getDeclaredMethods()) {
if (method.isAnnotationPresent(Id.class) || method.isAnnotationPresent(EmbeddedId.class)) {
idObject = JSON.parseObject(id, method.getGenericReturnType());
break;
}
}
//get dao class from spring
Object daoClass = null;
try {
daoClass = SpringUtil.getBean(Class.forName("com.example.db.dao." + entityType.getSimpleName() + "DAO"));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//get the entity with given primary key
JpaRepository simpleJpaRepository = (JpaRepository) daoClass;
Object entity = simpleJpaRepository.findOne((Serializable) idObject);
return (Serializable) entity;
}
#Override
public String toRequestId(Serializable id, Class<?> entityType) {
if (id == null) {
return null;
}
String jsonString = JSON.toJSONString(id);
String encodedString = "";
try {
encodedString = URLEncoder.encode(jsonString, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return encodedString;
}
}
After that. you can do what you want.
There is a sample below.
If the entity has single property pk, you can use
localhost:8080/demo/1 as normal. According to my code, suppose the pk
has annotation "#Id".
If the entity has composed pk, suppose the pk is demoId type, and has
annotation "#EmbeddedId", you can use localhost:8080/demo/{demoId
json} to get/put/delete. And your self link will be the same.
The answers provides above are helpful, but if you need a more generic approach that would be following -
package com.pratham.persistence.config;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.istack.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import javax.persistence.EmbeddedId;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Base64;
import java.util.Optional;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Customization of how composite ids are exposed in URIs.
* The implementation will convert the Ids marked with {#link EmbeddedId} to base64 encoded json
* in order to expose them properly within URI.
*
* #author im-pratham
*/
#Component
#RequiredArgsConstructor
public class EmbeddedBackendIdConverter implements BackendIdConverter {
private final ObjectMapper objectMapper;
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
return getFieldWithEmbeddedAnnotation(entityType)
.map(Field::getType)
.map(ret -> {
try {
String decodedId = new String(Base64.getUrlDecoder().decode(id));
return (Serializable) objectMapper.readValue(decodedId, (Class) ret);
} catch (JsonProcessingException ignored) {
return null;
}
})
.orElse(id);
}
#Override
public String toRequestId(Serializable id, Class<?> entityType) {
try {
String json = objectMapper.writeValueAsString(id);
return Base64.getUrlEncoder().encodeToString(json.getBytes(UTF_8));
} catch (JsonProcessingException ignored) {
return id.toString();
}
}
#Override
public boolean supports(#NonNull Class<?> entity) {
return isEmbeddedIdAnnotationPresent(entity);
}
private boolean isEmbeddedIdAnnotationPresent(Class<?> entity) {
return getFieldWithEmbeddedAnnotation(entity)
.isPresent();
}
#NotNull
private static Optional<Field> getFieldWithEmbeddedAnnotation(Class<?> entity) {
return Arrays.stream(entity.getDeclaredFields())
.filter(method -> method.isAnnotationPresent(EmbeddedId.class))
.findFirst();
}
}

Resources