Related
Spring Boot 2.4.0, DB is MySql 8.
Data is fetched every 15 seconds from remote with REST and storing it to MySql DB with saveAll().
Which call the save() method for all the given entities.
All data has set ID.
And I am expecting that if there is no such id at DB - it will be inserted.
If such ID is already presented at DB - it will be updated.
Here is snipped from the console:
Hibernate:
insert
into
iot_entity
(controller_ref, description, device_id, device_ref, entity_type_ref, hw_address, hw_serial, image_ref, inventory_nr, ip6address1, ip6address2, ip_address1, ip_address2, latlng, location, mac_address, name, params, status, tenant, type, id)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
...
2020-12-05 23:18:28.269 ERROR 15752 --- [ restartedMain] o.h.e.jdbc.batch.internal.BatchingBatch : HHH000315: Exception executing batch [java.sql.BatchUpdateException: Duplicate entry '1' for key 'iot_entity.PRIMARY'], SQL: insert into iot_entity (controller_ref, description, device_id, device_ref, entity_type_ref, hw_address, hw_serial, image_ref, inventory_nr, ip6address1, ip6address2, ip_address1, ip_address2, latlng, location, mac_address, name, params, status, tenant, type, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2020-12-05 23:18:28.269 WARN 15752 --- [ restartedMain] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1062, SQLState: 23000
2020-12-05 23:18:28.269 ERROR 15752 --- [ restartedMain] o.h.engine.jdbc.spi.SqlExceptionHelper : Duplicate entry '1' for key 'iot_entity.PRIMARY'
2020-12-05 23:18:28.269 DEBUG 15752 --- [ restartedMain] o.s.orm.jpa.JpaTransactionManager : Initiating transaction rollback after commit exception
org.springframework.dao.DataIntegrityViolationException: could not execute batch; SQL [insert into iot_entity (controller_ref, description, device_id, device_ref, entity_type_ref, hw_address, hw_serial, image_ref, inventory_nr, ip6address1, ip6address2, ip_address1, ip_address2, latlng, location, mac_address, name, params, status, tenant, type, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)]; constraint [iot_entity.PRIMARY]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute batch
Here is how to fetch and to save look like:
#Override
#SneakyThrows
#Scheduled(fixedDelay = 15_000)
#Transactional(propagation = Propagation.REQUIRES_NEW)
public void fetchAndStoreData() {
IotEntity[] entities = restTemplate.getForObject(properties.getIotEntitiesUrl(), IotEntity[].class);
log.debug("ENTITIES:\n{}", mapper.writerWithDefaultPrettyPrinter().writeValueAsString(entities));
if (entities != null && entities.length > 0) {
entityRepository.saveAll(List.of(entities));
} else {
log.warn("NO entities data FETCHED !!!");
}
}
This method runs every 15 seconds.
Entity:
#Data
#Entity
#NoArgsConstructor
#EqualsAndHashCode(of = {"id"})
#ToString(of = {"id", "deviceId", "entityTypeRef", "ipAddress1"})
public class IotEntity implements Serializable {
private static final long serialVersionUID = 1L;
#Id
private Integer id;
// other fields
and Repository:
public interface EntityRepository extends JpaRepository<IotEntity, Integer> {
}
Here is snipped for iot entity at JSON format:
2020-12-05 23:18:44.261 DEBUG 15752 --- [pool-3-thread-1] EntityService : ENTITIES:
[ {
"id" : 1,
"controllerRef" : null,
"name" : "Local Controller Unterföhring",
"description" : "",
"deviceId" : "",
...
So ID is definitely set.
Also, batching is enabled for a project. It shouldn't have any impact on saving.
I could not understand why it tries to insert a new entity instead of update the existing one?
Why it couldn't distinguish the difference between the old and new entities?
UPDATE:
Implemented Persistable for Entity:
#Data
#Entity
#NoArgsConstructor
#EqualsAndHashCode(of = {"id"})
#ToString(of = {"id", "deviceId", "entityTypeRef", "ipAddress1"})
public class IotEntity implements Serializable, Persistable<Integer> {
private static final long serialVersionUID = 1L;
#Id
private Integer id;
#Override
public boolean isNew() {
return false;
}
#Override
public Integer getId() {
return this.id;
}
However, it fails with the same exception - Duplicate entry '1' for key 'iot_entity.PRIMARY'
If I will add #GeneratedValue like the following:
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
It wouldn't fail. However, it will update the ID value by itself.
For example, it fetched with id = 15:
[ {
"id" : 15,
"carParkRef" : 15,
"name" : "UF Haus 1/2",
And should be saved like following:
In fact it has id = 2 instead:
And it is incorrect.
Tried to add to storing service:
private final EntityManager entityManager;
...
List.of(carParks).forEach(entityManager::merge);
Fails with the same exception (with or without implementing Persistable). It tries to insert the value - insert into ... Duplicate entry '15' for key '... .PRIMARY'
Snippet from application.yml:
spring:
# ===============================
# = DATA SOURCE
# ===============================
datasource:
url: jdbc:mysql://localhost:3306/demo_db
username: root
password: root
initialization-mode: always
# ===============================
# = JPA / HIBERNATE
# ===============================
jpa:
show-sql: true
generate-ddl: true
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
generate_statistics: true
Here you could see pom file content.
How to fix this issue?
The problem is likely that, since the #Id is not marked with #GeneratedValue, Spring Data assumes all detached (transient) entities passed to save()/saveAll() should have EntityManager.persist() invoked on them.
Try making IotEntity implement Persistable and returning false from isNew(). This will tell Spring Data to always use EntityManager.merge() instead, which should have the desired effect (i.e. inserting nonexistent entities and updating existing ones).
Looks like I found the root of this behaviour.
Main App launcher look like:
#AllArgsConstructor
#SpringBootApplication
public class Application implements CommandLineRunner {
private final DataService dataService;
private final QrReaderServer qrReaderServer;
private final MonitoringService monitoringService;
#Override
public void run(String... args) {
dataService.fetchAndStoreData();
monitoringService.launchMonitoring();
qrReaderServer.launchServer();
}
All 3 steps have strict execution sequence. And the first one has to repeat for updating data locally if it is needed. Two other just servers which work with stored data only.
Where the first method look like:
#Scheduled(fixedDelay = 15_000)
public void fetchAndStoreData() {
log.debug("START_DATA_FETCH");
carParkService.fetchAndStoreData();
entityService.fetchAndStoreData();
assignmentService.fetchAndStoreData();
permissionService.fetchAndStoreData();
capacityService.fetchAndStoreData();
log.debug("END_DATA_FETCH");
}
Also, this execution is scheduled as well.
When the app starts it tried to execute this fetching twice:
2020-12-14 14:00:46.208 DEBUG 16656 --- [pool-3-thread-1] c.s.s.s.data.impl.DataServiceImpl : START_DATA_FETCH
2020-12-14 14:00:46.208 DEBUG 16656 --- [ restartedMain] c.s.s.s.data.impl.DataServiceImpl : START_DATA_FETCH
2 threads run at the same catch and store in parallel - trying to insert data. (tables are recreated at every start).
All later fetches are fine, they are executed only by #Sceduled thread.
If comment #Sceduled - it will work fine without any Exceptions.
SOLUTION:
Added additional boolean property to service class:
#Getter
private static final AtomicBoolean ifDataNotFetched = new AtomicBoolean(true);
#Override
#Scheduled(fixedDelay = 15_000)
#Order(value = Ordered.HIGHEST_PRECEDENCE)
public void fetchAndStoreData() {
ifDataNotFetched.set(true);
log.debug("START_DATA_FETCH");
// fetch and store data with `saveAll()`
log.debug("END_DATA_FETCH");
ifDataNotFetched.set(false);
}
And control the value after the application is started:
#Value("${sharepark.remote-data-fetch-timeout}")
private int dataFetchTimeout;
private static int fetchCounter;
#Override
public void run(String... args) {
waitRemoteDataStoring();
monitoringService.launchMonitoring();
qrReaderServer.launchServer();
}
private void waitRemoteDataStoring() {
do {
try {
if (fetchCounter == dataFetchTimeout) {
log.warn("Data fetch timeout reached: {}", dataFetchTimeout);
}
Thread.sleep(1_000);
++fetchCounter;
log.debug("{} Wait for data fetch one more second...", fetchCounter);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} while (DataServiceImpl.getIfDataNotFetched().get() && fetchCounter <= dataFetchTimeout);
}
Spring Data JPA uses combination of #version #Id field to decide the whether to merge or insert.
null #id and null #version would mean new record hence insert
if #id is present #version field is used to decide whether to merge or insert.
Update is only invoked when (update .... where id = xxx and version = 0)
Beacuse you have #id and #version missing, its trying to insert, because underlysing system decided this is new record and when run sql u get error.
Can you pls try with #GeneratedValue(strategy = GenerationType.AUTO)
This worked for me.
I have two tables with a one-to-many relationship. I want to fetch those records and insert into another database which having same table by changing the primary key.
My application entity class
#Entity
#Table(name = "EM_APPLICATION")
public class ApplicationTable {
#Id
private int APPLICATION_ID;
#Id
private String CUSTOMER_ID;
private String LAST_NAME;
private String FIRST_NAME;
#OneToMany( fetch = FetchType.EAGER,cascade = CascadeType.ALL)
#JoinColumns({ #JoinColumn(name = "CUSTOMER_ID", referencedColumnName = "CUSTOMER_ID"),
#JoinColumn(name = "APPLICATION_ID", referencedColumnName = "APPLICATION_ID") })
private Set<AddressTable> address;
//Getters and setters
}
Address entity class..
#Entity
#Table(name="EM_APPL_ADDRESS")
public class AddressTable{
#Id
private int APPLICATION_ID;
#Id
private String CUSTOMER_ID;
#Id
private String ADDRESS_TYPE;
//Getters and setters
}
I have to execute a method for fetching records from DB using hibernate:
public void execute(String applId, String customerId) {
Session session = HibernateQAUtil.openSession();
Transaction tx = session.beginTransaction();
String hql = "FROM ApplicationTable WHERE CUSTOMER_ID =:CUSTOMER_ID AND APPLICATION_ID =:APPLICATION_ID";
Query query = session.createQuery(hql);
query.setParameter("CUSTOMER_ID", customerId);
query.setParameter("APPLICATION_ID", Integer.parseInt(applId));
List<ApplicationTable> list = query.list();
tx.commit();
session.close();
ApplicationTable applVO = list.get(0);
insertApplication(applVO );
}
After fetching the records, I am changing APPLICATION_ID, CUSTOMER_ID and some other columns in address table and after inserting in another database.
private void insertApplication(ApplicationTable emApplVO) {
applVO.setAPPLICATION_ID(123456);
applVO.setCUSTOMER_ID("88888888");
Set<AddressTable> addressSet = emApplVO.getAddress();
for (AddressTable address : addressSet) {
address.setAPPLICATION_ID(123456);
address.setCUSTOMER_ID("88888888");
address.setZIP(500032);
}
Session session1 = HibernateUtil.openSession();
Transaction beginTransaction = session1.beginTransaction();
session1.save(emApplVO);
beginTransaction.commit();
session1.close();
}
Hibernate queries in console log are... (below mentioned queries are too large so copied to some extent only..)
Hibernate: select em_applica0_.CUSTOMER_ID as CUSTOMER1_0_,em_applica0_.APPLICATION_ID as APPLICAT2_0_,em_applica0_.ARCHIVE_IND as ARCHIVE8_0_ where em_applica0_.CUSTOMER_ID=? and em_applica0_.APPLICATION_ID=?
Hibernate: select address0_.CUSTOMER_ID as CUSTOMER1_0_1_, address0_.APPLICATION_ID as APPLICAT2_0_1_, address0_.ADDRESS_TYPE as ADDRESS3_1_0_ where em_applica0_.CUSTOMER_ID=? and em_applica0_.APPLICATION_ID=?
Hibernate: insert into EM_APPLICATION (CUSTOMER_ID, APPLICATION_ID, APPLICATION_NBR, APPLICATION_STATUS, APPLICATION_TYPE) values (?, ?, ?, ?)
Hibernate: insert into EM_APPL_ADDRESS (CUSTOMER_ID, APPLICATION_ID, ADDRESS_TYPE) values (?, ?, ?)
Question 1: in the insert method, I have assigned address to addresSet and made some changes in addresSet, after making those changes, I am not assigned the addressSet to applVO (i.e. not written applVO.setAddress(addresSet )) but it inserted a record with updated values into the Address table. What is happening here?
When I am changing code inside insertApplication(ApplicationTable emApplVO) method to
private void insertApplication(ApplicationTable emApplVO) {
applVO.setAPPLICATION_ID(123456);
applVO.setCUSTOMER_ID("88888888");
Set<AddressTable> addressSet = emApplVO.getAddress();
Set<AddressTable> newAddressSet = new HashSet<AddressTable>();
for (AddressTable address : newAddressSet) {
address.setAPPLICATION_ID(emApplVO.getAPPLICATION_ID());
address.setCUSTOMER_ID(emApplVO.getCUSTOMER_ID());
address.setZIP(500032);
newAddressSet.add(address);
}
emApplVO.setAddress(null);
emApplVO.setAddress(newAddressSet);
Session session1 = HibernateUtil.openSession();
Transaction beginTransaction = session1.beginTransaction();
session1.save(emApplVO);
beginTransaction.commit();
session1.close();
}
Hibernate queries in console log are... It also executing update ...
Hibernate: select em_applica0_.CUSTOMER_ID as CUSTOMER1_0_,em_applica0_.APPLICATION_ID as APPLICAT2_0_,em_applica0_.ARCHIVE_IND as ARCHIVE8_0_ where em_applica0_.CUSTOMER_ID=? and em_applica0_.APPLICATION_ID=?
Hibernate: select address0_.CUSTOMER_ID as CUSTOMER1_0_1_, address0_.APPLICATION_ID as APPLICAT2_0_1_, address0_.ADDRESS_TYPE as ADDRESS3_1_0_ where em_applica0_.CUSTOMER_ID=? and em_applica0_.APPLICATION_ID=?
Hibernate: insert into EM_APPLICATION (CUSTOMER_ID, APPLICATION_ID, APPLICATION_NBR, APPLICATION_STATUS, APPLICATION_TYPE) values (?, ?, ?, ?)
Hibernate: insert into EM_APPL_ADDRESS (CUSTOMER_ID, APPLICATION_ID, ADDRESS_TYPE) values (?, ?, ?)
update EM_APPL_ADDRESS set CUSTOMER_ID=?, APPLICATION_ID=? where CUSTOMER_ID=? and APPLICATION_ID=? and ADDRESS_TYPE=?
Question 2: why is the update query executed?
Question 3: while using List<AddressTable> instead of Set<AddressTable>, I got some errors. What is the difference?
I have the following Entity Relations:
#AllArgsConstructor
#NoArgsConstructor
#Builder
#Data
#Entity
public class BatchProcessRecord implements Serializable, Cloneable {
...
#ManyToOne
#JoinColumn(name = "batch_process_id")
private BatchProcess batchProcess;
}
#AllArgsConstructor
#NoArgsConstructor
#Builder
#Data
#Entity
public class BatchProcess implements Serializable, Cloneable {
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "batchProcess")
private List<BatchProcessRecord> records;
}
And the following code that saves the entities:
// Create batch process
BatchProcess batchProcess = BatchProcess.builder()
....
.records(records)
.build();
// Save entity
batchProcessRepository.save(batchProcess);
In the console, it generates the inserts as expected (with 2 children), but without filling batch_process_id (JoinColumn).
What is happening?
Console:
Hibernate: insert into batch_process (batch_end_date, batch_entries_count, batch_entries_with_issues, batch_name, batch_start_date, report_file_download_date, report_file_name, result_file_name, result_file_upload_date, status) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into batch_process_record (address_number, batch_process_id, city, first_name, full_street_address, last_name, maternal_name, post_directional, pre_directional, reference_number, ssn, state, street_type, zip_code) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into batch_process_record (address_number, batch_process_id, city, first_name, full_street_address, last_name, maternal_name, post_directional, pre_directional, reference_number, ssn, state, street_type, zip_code) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2
Repository:
public interface BatchProcessRepository extends
JpaRepository<BatchProcess, Long> {
}
I have a problem when trying to persist new entities. I'm using Eclipselink 2.4.2 as entity manager. My BaseDao class in the store method flushes and refreshes entity after persisting it as new (persist->flush->refresh). All is happening in a single transaction.
My entities look like this (the part I'm concerned about):
TrustEntity {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "trust", cascade = {CascadeType.ALL})
#PrivateOwned
private List<TrustIncentiveRateEntity> trustIncentiveRates;
}
TrustIncentiveRateEntity {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "TRUST_ID", nullable = false)
private TrustEntity trust;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "trustIncentiveRate", cascade = {CascadeType.ALL})
#PrivateOwned
private List<TrustIncentiveRateValueEntity> trustIncentiveRateValues;
}
TrustIncentiveRateValueEntity {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "TRUST_INCENTIVE_RATE_ID", nullable = false)
private TrustIncentiveRateEntity trustIncentiveRate;
}
I'm creating a new Trust entity, instantiate a TrustIncentiveRateEntity list, create one new element in it, instantiate a TrustIncentiveRateEntity and create one new element in it.
During debugging I could see that all the references, in both ways, are correct.
Now, when I try to persist this here is what happens:
Log from server:
FINE: SELECT SEQ_TRUST.NEXTVAL FROM DUAL
FINE: SELECT SEQ_TRUST_INCENTIVE_RATE.NEXTVAL FROM DUAL
FINE: SELECT SEQ_TRUST_INCENTIVE_RATE_VALUE.NEXTVAL FROM DUAL
FINE: INSERT INTO TRUST (TRUST_ID, ACTION_DATE, ACTION_USER_ID, IS_ACTIVE, CREATION_DATE, CREATION_USER_ID, IS_INCENTIVE_ACTIVE, IS_NO_CREDIT_LIMIT, PROCESS_CURRENT_STATUS, REMARKS, STATUS_INCENTIVE, TRUST_CODE, TRUST_NAME, TRUST_TYPE, UPDATE_DATE, UPDATE_USER_ID, VERSION_OPT_LOCK, STRUCTURAL_ORG_UNIT_SALES_ID, STRUCTURAL_ORG_UNIT_ID, USER_ID) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
bind => [38007, 2013-04-26 09:46:31.582, 1003186, true, 2013-04-26 07:46:34.659, 1003186, true, false, OPEN, null, OPEN, 100058, 741963852, T, null, null, 1, 387, 387, 1003186]
FINE: INSERT INTO TRUST_INCENTIVE_RATE (TRUST_INCENTIVE_RATE_ID, CREATION_DATE, CREATION_USER_ID, EQUIPMENT_SIZE, EXTENDED_EQ_GROUP_ID, RATE_BASIS, UPDATE_DATE, UPDATE_USER_ID, VERSION_OPT_LOCK, TRADE_ID, TRUST_ID) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
bind => [15001, 2013-04-26 07:46:39.862, 1003186, 20, 2, B, null, null, 1, 144001, 38007]
FINE: INSERT INTO TRUST_INCENTIVE_RATE_VALUE (TRUST_INCENTIVE_RATE_VALUE_ID, CREATION_DATE, CREATION_USER_ID, EFFECTIVE_DATE, EXPIRY_DATE, INCENTIVE, STATUS, UPDATE_DATE, UPDATE_USER_ID, VERSION_OPT_LOCK, CURRENCY_CODE, TRUST_INCENTIVE_RATE_ID) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
bind => [14007, 2013-04-26 07:46:39.955, 1003186, 2013-04-26 00:00:00.0, 9999-12-31 00:00:00.0, 12, OPEN, null, null, 1, USD, 15001]
FINE: SELECT TRUST_ID, ACTION_DATE, ACTION_USER_ID, IS_ACTIVE, CREATION_DATE, CREATION_USER_ID, IS_INCENTIVE_ACTIVE, IS_NO_CREDIT_LIMIT, PROCESS_CURRENT_STATUS, REMARKS, STATUS_INCENTIVE, TRUST_CODE, TRUST_NAME, TRUST_TYPE, UPDATE_DATE, UPDATE_USER_ID, VERSION_OPT_LOCK, STRUCTURAL_ORG_UNIT_SALES_ID, STRUCTURAL_ORG_UNIT_ID, USER_ID FROM TRUST WHERE (TRUST_ID = ?)
bind => [38007]
FINE: SELECT TRUST_INCENTIVE_RATE_ID, CREATION_DATE, CREATION_USER_ID, EQUIPMENT_SIZE, EXTENDED_EQ_GROUP_ID, RATE_BASIS, UPDATE_DATE, UPDATE_USER_ID, VERSION_OPT_LOCK, TRADE_ID, TRUST_ID FROM TRUST_INCENTIVE_RATE WHERE (TRUST_ID = ?)
bind => [38007]
FINE: SELECT TRUST_INCENTIVE_RATE_VALUE_ID, CREATION_DATE, CREATION_USER_ID, EFFECTIVE_DATE, EXPIRY_DATE, INCENTIVE, STATUS, UPDATE_DATE, UPDATE_USER_ID, VERSION_OPT_LOCK, CURRENCY_CODE, TRUST_INCENTIVE_RATE_ID FROM TRUST_INCENTIVE_RATE_VALUE WHERE (TRUST_INCENTIVE_RATE_ID = ?)
bind => [15001]
So far so good, but when the transaction is commited by EntityManager I get the following exception:
WARNING: java.lang.IllegalStateException: During synchronization a new object was found through a relationship that was not marked cascade PERSIST: my.package.entity.TrustIncentiveRateEntity#1fa1df7.
at org.eclipse.persistence.internal.sessions.RepeatableWriteUnitOfWork.discoverUnregisteredNewObjects(RepeatableWriteUnitOfWork.java:303)
at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.calculateChanges(UnitOfWorkImpl.java:706)
at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.commitToDatabaseWithChangeSet(UnitOfWorkImpl.java:1498)
at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.issueSQLbeforeCompletion(UnitOfWorkImpl.java:3151)
at org.eclipse.persistence.internal.sessions.RepeatableWriteUnitOfWork.issueSQLbeforeCompletion(RepeatableWriteUnitOfWork.java:345)
at org.eclipse.persistence.transaction.AbstractSynchronizationListener.beforeCompletion(AbstractSynchronizationListener.java:158)
at org.eclipse.persistence.transaction.JTASynchronizationListener.beforeCompletion(JTASynchronizationListener.java:68)
at com.sun.enterprise.transaction.JavaEETransactionImpl.commit(JavaEETransactionImpl.java:435)
at com.sun.enterprise.transaction.JavaEETransactionManagerSimplified.commit(JavaEETransactionManagerSimplified.java:855)
at com.sun.ejb.containers.BaseContainer.completeNewTx(BaseContainer.java:5136)
at com.sun.ejb.containers.BaseContainer.postInvokeTx(BaseContainer.java:4901)
at com.sun.ejb.containers.BaseContainer.postInvoke(BaseContainer.java:2045)
at com.sun.ejb.containers.BaseContainer.postInvoke(BaseContainer.java:1994)
at com.sun.ejb.containers.EJBLocalObjectInvocationHandler.invoke(EJBLocalObjectInvocationHandler.java:222)
at com.sun.ejb.containers.EJBLocalObjectInvocationHandlerDelegate.invoke(EJBLocalObjectInvocationHandlerDelegate.java:88)
at $Proxy241.save(Unknown Source)
Which for me seems strange, like the EM tries to actually store TrustIncentiveRateValueEntity before TrustIncentiveRateEntity and can't see TrustIncentiveRateEntity.
After looking at similar threads I've added CascadeType.PERSIST to the #ManyToOne annotation over trustIncentiveRate field in TrustIncentiveRateValueEntity class. After that the situation looks like this: EM inserts the entities like before AND THEN it gets nextval from SEQ_TRUST_INCENTIVE_RATE and tries to insert TrustIncentiveRateValueEntity again (with the new id, but the rest of the field values remain the same). It results in constrain violation, as I have a unique constrain on cross-section of some of this table columns. Exception, transaction rolled back, I am still sad.
My store method in the BaseDao class:
#TransactionAttribute(TransactionAttributeType.MANDATORY)
public T_ENTITY store(T_ENTITY entity) {
if (!entity.isNewlyCreated()) {
T_ENTITY mergedEntity = em.merge(entity);
flush();
return mergedEntity;
} else {
try {
em.persist(entity);
flush();
refresh();
} catch (RuntimeException exc) {
entity.resetPersistentFlag();
throw exc;
}
return entity;
}
}
But calling the em.persist(entity) directly, without flush/refresh causes the same problem.
The logic of the service call:
#Override
#TransactionAttribute(TransactionAttributeType.REQUIRED)
public TrustEntity save(TrustEntity dto) {
TrustEntity trust = trustDao.store(trust);
workflowConversation.triggerWorkflow(); // doesn't do anything to any of the entities when they are freshly created
return trust;
}
Anybody could help me in identifying what could be wrong with this?
You cannot have a OneToMany that uses both a mappedby, making it bidirectional, and a joincolumn marking it as unidirectional. They conflict and are causing you issues.
When you mark a relationship as mappedby, you specify that all information and control of the relationship is on the other side - that includes the joincolumn info. try:
TrustEntity {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "trust", cascade = {CascadeType.ALL})
#PrivateOwned
private List<TrustIncentiveRateEntity> trustIncentiveRates;
}
TrustIncentiveRateEntity {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "TRUST_ID", nullable = false)
private TrustEntity trust;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "trustIncentiveRate", cascade = {CascadeType.ALL})
#PrivateOwned
private List<TrustIncentiveRateValueEntity> trustIncentiveRateValues;
}
I want to persist a mail entity which has some resources (inline or attachment). First I related them as a bidirectional relation:
#Entity
public class Mail extends BaseEntity {
#OneToMany(mappedBy = "mail", cascade = CascadeType.ALL, orphanRemoval = true)
private List<MailResource> resource;
private String receiver;
private String subject;
private String body;
#Temporal(TemporalType.TIMESTAMP)
private Date queued;
#Temporal(TemporalType.TIMESTAMP)
private Date sent;
public Mail(String receiver, String subject, String body) {
this.receiver = receiver;
this.subject = subject;
this.body = body;
this.queued = new Date();
this.resource = new ArrayList<>();
}
public void addResource(String name, MailResourceType type, byte[] content) {
resource.add(new MailResource(this, name, type, content));
}
}
#Entity
public class MailResource extends BaseEntity {
#ManyToOne(optional = false)
private Mail mail;
private String name;
private MailResourceType type;
private byte[] content;
}
And when I saved them:
Mail mail = new Mail("asdasd#asd.com", "Hi!", "...");
mail.addResource("image", MailResourceType.INLINE, someBytes);
mail.addResource("documentation.pdf", MailResourceType.ATTACHMENT, someOtherBytes);
mailRepository.save(mail);
Three inserts were executed:
INSERT INTO MAIL (ID, BODY, QUEUED, RECEIVER, SENT, SUBJECT) VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO MAILRESOURCE (ID, CONTENT, NAME, TYPE, MAIL_ID) VALUES (?, ?, ?, ?, ?)
INSERT INTO MAILRESOURCE (ID, CONTENT, NAME, TYPE, MAIL_ID) VALUES (?, ?, ?, ?, ?)
Then I thought it would be better using only a OneToMany relation. No need to save which Mail is in every MailResource:
#Entity
public class Mail extends BaseEntity {
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "mail_id")
private List<MailResource> resource;
...
public void addResource(String name, MailResourceType type, byte[] content) {
resource.add(new MailResource(name, type, content));
}
}
#Entity
public class MailResource extends BaseEntity {
private String name;
private MailResourceType type;
private byte[] content;
}
Generated tables are exactly the same (MailResource has a FK to Mail). The problem is the executed SQL:
INSERT INTO MAIL (ID, BODY, QUEUED, RECEIVER, SENT, SUBJECT) VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO MAILRESOURCE (ID, CONTENT, NAME, TYPE) VALUES (?, ?, ?, ?)
INSERT INTO MAILRESOURCE (ID, CONTENT, NAME, TYPE) VALUES (?, ?, ?, ?)
UPDATE MAILRESOURCE SET mail_id = ? WHERE (ID = ?)
UPDATE MAILRESOURCE SET mail_id = ? WHERE (ID = ?)
Why this two updates? I'm using EclipseLink, will this behaviour be the same using another JPA provider as Hibernate? Which solution is better?
UPDATE:
- If I don't use #JoinColumn EclipseLink creates three tables: MAIL, MAILRESOURCE and MAIL_MAILRESOURCE. I think this is perfectly logic. But with #JoinColumn it has information enough for creating only two tables and, in my opinion, do only inserts, with no updates.
When you use a #JoinColumn in a OneToMany you are defining a "unidirectional" one to many, which is a new type of mapping added in JPA 2.0, this was not supported in JPA 1.0.
This is normally not the best way to define a OneToMany, a normal OneToMany is defined using a mappedBy and having a ManyToOne in the target object. Otherwise the target object has no knowledge of this foreign key, and thus the separate update for it.
You can also use a JoinTable instead of the JoinColumn (this is the default for OneToMany), and then there is no foreign key in the target to worry about.
There is also a fourth option. You could mark the MailResource as an Embeddable instead of Entity and use an ElementCollection.
See,
http://en.wikibooks.org/wiki/Java_Persistence/OneToMany
Mapped by defines owning side of the relation ship so for JPA it gives better way to handle associations. Join Column only defines the relationship column. Since JPA is completely reflection based framework I could think of the optimization done for Mapped by since it is easy find owning side this way.