Inconsistent Owned Hibernate Object - spring-boot

posting for a colleague, we’ve encountered unexpected data fetching behaviour and would like to ask for any ideas on how and why it could be done this way. The problem is that we have managed entities with the same id but in a different state. Unfortunately, it’s not reproducible locally. We can see that behaviour from the AWS-hosted EC2 Intel Instance and are only able to verify it from logs.
Used classes:
class History {
private Enum status;
#OneToMany(mappedBy = HistoryEntry_.HISTORY)
#OrderBy("id ASC")
#BatchSize(size = 10)
private List<HistoryEntry> entries = new LinkedList<>();
}
// binds 2 parts of the system
class HistoryEntry {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "history_id", updatable = false, nullable = false)
private History history;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "process_id", updatable = false, nullable = false)
private ProcessInfo processInfo;
}
class ProcessInfo {
#OneToMany(mappedBy = HistoryEntry_.PROCESS_INFO)
// just for deterministic ordering within the transaction
#OrderBy("id ASC")
#BatchSize(size = 10)
private List<HistoryEntry> entries = new LinkedList<>();
}
Initial State
History history1 = new History(Status.PENDING);
ProcessInfo processInfo1 = new ProcessInfo();
ProcessInfo processInfo2 = new ProcessInfo();
HistoryEntry entryA = new HistoryEntry(history, processInfo1);
HistoryEntry entryB = new HistoryEntry(history, processInfo1);
History1 in PENDING status containing a list of [A, B] entries
ProcessInfo1 containing a list of [A, B] entries
ProcessInfo2 without entries
New Entry linking to History1
The execution runs in a single transaction
// returns a list with 2 entries A and B
List<HistoryEntry> entries = historyEntryRepo.findAllByProcessInfoAndNotArchived(processInfo1);
// a set with a single history1. Comparison done by id.
Set<History> histories = entries.stream()
.map(HistoryEntry::getHistory)
.toSet();
// expecting a single element
if(histories.size() > 1){
throw new IllegalStateException();
}
History history1 = histories.iterator().next();
entityManager.lock(history1, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
HistoryEntry entryC = new HistoryEntry();
entryC.setHistory(history1);
history1.getEntries().add(entryC);
history1.status = Status.COMPLETED;
entryC.setProcessInfo(processInfo2);
processInfo2.getEntries().add(entryC);
historyEntryRepo.save(entryC);
Inconsistent Data
Within the same transaction, I want to verify the status of each history entity.
Set<History> histories1 = processInfo1.getEntries().stream()
.map(HistoryEntry::getHistory)
.toSet();
Set<History> histories2 = processInfo2.getEntries().stream()
.map(HistoryEntry::getHistory)
.toSet();
Both return a single result - History1 - with the same id but with a different status field value. This is unexpected, to say the least.
History(id=1, status=PENDING) unexpected
History(id=1, status=COMPLETED)
Reloading for Consistency
It’s “fixed” by doing a fetch from the repo anew.
historyEntryRepo.findAllByProcessInfo(processInfo1).stream()
.map(HistoryEntry::getHistory)
.toSet();
historyEntryRepo.findAllByProcessInfo(processInfo2).stream()
.map(HistoryEntry::getHistory)
.toSet();
Now, the outcome is the same and expected: History(id=1, status=COMPLETED)
Is this some caching or rather cache-invalidation gone wrong?

Related

How to generate id field value within specific range in spring data jpa

Is there any way that I can generate ID field as 4 digit number i.e from 1000 to 9999 in my Spring boot application. Current Id field looks like this:
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
#Column(name = "EMP_ID", nullable = false)
public short getEmp_id() {
return emp_id;
}
As of now id is getting generated from 1. But I wanted to get it generated starting from 1000 and incremented by 1 until 9999.
As suggest by Ishikawa in comments and by referring Sequence Generation from Sequence Generation did below changes:
#Id
#GenericGenerator(
name = "empid-sequence-generator",
strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",
parameters = {
#Parameter(name = "sequence_name", value = "user_sequence"),
#Parameter(name = "initial_value", value = "1000"),
#Parameter(name = "increment_size", value = "1")
}
)
#GeneratedValue(generator = "empid-sequence-generator")
#Column(name = "EMP_ID", nullable = false)
public short getEmp_id() {
return emp_id;
}
but even after that when trying to save the emp getting the below exception:
com.microsoft.sqlserver.jdbc.SQLServerException: Invalid object name 'user_sequence'.
at com.microsoft.sqlserver.jdbc.SQLServerException.makeFromDatabaseError(SQLServerException.java:262)
at com.microsoft.sqlserver.jdbc.SQLServerStatement.getNextResult(SQLServerStatement.java:1624)
at com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement.doExecutePreparedStatement(SQLServerPreparedStatement.java:594)
at com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement$PrepStmtExecCmd.doExecute(SQLServerPreparedStatement.java:524)
at com.microsoft.sqlserver.jdbc.TDSCommand.execute(IOBuffer.java:7194)
at com.microsoft.sqlserver.jdbc.SQLServerConnection.executeCommand(SQLServerConnection.java:2979)
at com.microsoft.sqlserver.jdbc.SQLServerStatement.executeCommand(SQLServerStatement.java:248)
at com.microsoft.sqlserver.jdbc.SQLServerStatement.executeStatement(SQLServerStatement.java:223)
NOTE: It's third party database so I can't do any schema/constraint changes.I need to handle this through java code only.
My bad. Forgot to uncomment below line in application.properties.
spring.jpa.hibernate.ddl-auto = update
After uncommenting when I reboot my application it created the "user_sequence".

JPA Specification sort by native SQL field

I'm using Spring Boot v1.5.3
In my code, I have a search operation with a lot of conditions.
public Page<ParentObject> search(Pageable pageable) {
Specification<ParentObject> specification = (root, cq, cb) -> {
Predicate p = cb.and(
cb.equals(root.get("child").get("id"), "someValue"),
// a lot of predicates appended by conditions
);
return p;
};
Sort newSort = pageable.getSort().and(new Sort(Sort.Direction.ASC, "child.id"));
pageable = new PageRequest(pageable.getPageNumber(), pageable.getPageSize(), newSort)
Page<ParentObject> result = parentObjectRepository.findAll(specification, pageable);
return result;
}
The problem is that my parent table contains child_id field with index. And I want SQL to be like:
SELECT .... FROM parent p INNER JOIN child c ON c.id = p.child_id WHERE ...
ORDER BY p.child_id ASC;
But in result I have:
SELECT .... FROM parent p INNER JOIN child c ON c.id = p.child_id WHERE ...
ORDER BY c.id ASC;
Pay attention to ORDER BY clause. If I have c.id index is not involved and the search is slow. If I have ORDER BY p.child_id it works much faster.
I tried to use
Sort newSort = pageable.getSort().and(new Sort(Sort.Direction.ASC, "child"));
but it does not work as expected.
Entity:
public class ParentObject {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "child_id", referencedColumnName = "id")
private ChildObject child;
}
I can't replace this by native SQL, because this search specification contains 30+ if/else statements and it will take a lot of time to rewrite the code.
How can I solve this problem? Thanks in advance for your answers.
Finally, I found a solution. As we know, ORM works with database thru entities.
All my entities have String (UUID) values as ids. So, I have added the following code into my ParentObject:
public class ParentObject {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "child_id", referencedColumnName = "id")
private ChildObject child;
#Column(name = "child_id", insertable = false, updatable = false)
private String childId;
}
As you see, here I have added simple field that contains id of child object. It's important to mark this field as insertable = false, updatable = false and we can't change this field in the code, this is read-only field. But this allows us to sort results by parent.child_id, not by child.id. If we need to replace child object, need just call setChild(newValue).
And finally, my search method now looks like:
public Page<ParentObject> search(Pageable pageable) {
Specification<ParentObject> specification = (root, cq, cb) -> {
Predicate p = cb.and(
cb.equals(root.get("child").get("id"), "someValue"),
// a lot of predicates appended by conditions
);
return p;
};
// IMPORTANT change in next line
Sort newSort = pageable.getSort().and(new Sort(Sort.Direction.ASC, "childId"));
pageable = new PageRequest(pageable.getPageNumber(), pageable.getPageSize(), newSort)
Page<ParentObject> result = parentObjectRepository.findAll(specification, pageable);
return result;
}
And in result I have the following SQL query:
SELECT .... FROM parent p INNER JOIN child c ON c.id = p.child_id WHERE ...
ORDER BY p.child_id ASC;
Hope this would be useful for someone

Map new column from Spring Native query to entity

I have a case statement in my Native query where I am attempting to override a field in my entity.
SELECT i.id, i.ONE_TO_ONE_ID, i.ANOTHER, CASE(WHEN condition THEN 'YES' WHEN another_condition THEN 'NO' ELSE 'MAYBE' END) as word ....
I am using this with JpaRepository as a native query, with pagination.
When I run the native query against my db directly, the result set looks as though I expect.
| id_value | MAPPED_ENTITY_ID_value | another value | word_value (YES) |
When I run the native query from my JpaRepository, everything works there, except word is always null. I cant' seem to figure out how to map the additional String word result to a field in my Entity.
Is there a way to get this to map? Or will I have to create an entire #SqlResultSetMapping() for all of my fields coupled with a native query? (hoping not)
UPDATE: 1
I was generalizing above. Here is my Query.
#Query(
name = "listPagedMapping",
value = "SELECT DISTINCT i.ID, i.INSTANCE_ID, i.REGION, i.CNAME_STACK_ID, i.INSTANCE_STATE, i.IP_ADDRESS, i.EC2_ROLE_NAME, i.INSTANCE_OWNER, i.IS_MASTER, i.EC2_MASTER_ID, i.CNAME, i.EC2_START_TIMESTAMP, i.PRIVATE_DNS, i.INSTANCE_NAME, i.AUTO_TERMINATE, i.AUTO_TERMINATE_DATE, i.TERMINATION_ZONE, i.ADMIN_GROUP_AD_LDAP_ID, i.USER_GROUP_AD_LDAP_ID, (CASE WHEN i.INSTANCE_OWNER=:username THEN 'OWNER' WHEN i.ADMIN_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID) THEN 'ADMIN' WHEN i.USER_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID) THEN 'USER' END) as PERMISSION FROM USER u, USER_ACCESS_GROUPS g, EC2_PROVISIONING i WHERE i.INSTANCE_OWNER=:username and i.INSTANCE_STATE in (:instanceStates) or u.username=:username and i.INSTANCE_STATE in (:instanceStates) and g.USER_ID=u.USER_ID and (i.ADMIN_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID) or i.USER_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID))",
countQuery = "SELECT count(*) FROM (SELECT DISTINCT i.* FROM USER u, USER_ACCESS_GROUPS g, EC2_PROVISIONING i WHERE i.INSTANCE_OWNER=:username and i.INSTANCE_STATE in (:instanceStates) or u.username=:username and i.INSTANCE_STATE in (:instanceStates) and g.USER_ID=u.USER_ID and (i.ADMIN_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID) or i.USER_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID))) as ug",
nativeQuery = true)
Page<Ec2Instance> findAllByPermissionUserAdminOrOwnerAndInstanceStateIn(
#Param("username")final String username,
#Param("instanceStates") final Set<String> instanceStates,
final Pageable pageable);
}
Obviously a bit more complex.
I can get it to map to the entity field with using a named query, but then I loose all the default mappings:
#JsonInclude(JsonInclude.Include.NON_NULL)
#SuppressWarnings("unused")
#Data
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode(exclude={"masterNode", "workers", "associatedBuckets"})
#Entity
#Table(name = "EC2_PROVISIONING")
#SqlResultSetMapping(
name="listPagedMapping",
columns = {
#ColumnResult(name = "permission", type = String.class)
}
)
#NamedNativeQuery(
name = "listAccessibleInstances",
query = ACCESSIBLE_QUERY,
resultSetMapping = "listPagedMapping"
)
public class Ec2Instance {
....
private String permission;
#column(name = "INSTANCE_ID")
private String instanceId;
#ManyToOne
#JoinColumn(name = "EC2_MASTER_ID")
private Ec2Instance masterNode;
#Setter(AccessLevel.NONE)
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "WORKER_EC2_NODES", joinColumns = { #JoinColumn(name = "EC2_MASTER_ID") }, inverseJoinColumns = {
#JoinColumn(name = "ID") })
private Set<Ec2Instance> workers = new HashSet<>();
... More fields ..
}
I guess, I am hoping there is a way to provide a single mapping on-top of the default mapping that is done by ORM. The above code results in only a pageable of Content PERMISSION, rather than the whole entity + permission.
UPDATE: 2
Ok, so I am getting closer... Seems by removing the #ColumnResult I do get the default mapping, plus the PERMISSION field mapped over! Looks like this:
#SqlResultSetMapping(
name="listPagedMapping"
)
The last issue is it does not accept my CountQuery, and causes my tests to fail whenever a Pagination Query results with multiple pages. Looks like Spring try's to come up with its own CountQuery, which is not correct.
UPDATE: 3
To finish this off, looks like I can provide the Count Query as described here: Spring Data - Why it's not possible to have paging with native query
I will give this a go and update back.
I never got this to work quite how I wanted. I am sure I could by mapping my entire entity, but, that would have been painstaking. I ended up solving this by using NamedNativeQueries, with mapping for the additional Column as a result of my Case statement. My entity class is now annotated like:
#JsonInclude(JsonInclude.Include.NON_NULL)
#SuppressWarnings("unused")
#Data
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode(callSuper = false)
#Entity
#Table(name = "EC2_PROVISIONING")
#SqlResultSetMappings({
#SqlResultSetMapping(
name = "listPagedMapping",
entities = {
#EntityResult(
entityClass = Ec2Instance.class
)
},
columns = {#ColumnResult(name = "permission", type = String.class)}
),
#SqlResultSetMapping(name = "listPagedMapping.count", columns = #ColumnResult(name = "cnt"))
})
#NamedNativeQueries({
#NamedNativeQuery(
name = "Ec2Instance.listAccessibleInstances",
query = ACCESSIBLE_QUERY,
resultClass = Ec2Instance.class,
resultSetMapping = "listPagedMapping"
),
#NamedNativeQuery(
name = "Ec2Instance.listAccessibleInstances.count",
resultSetMapping = "listPagedMapping.count",
query = ACCESSIBLE_QUERY_COUNT
)
})
We also dont need the permission field in this entity anymore. I removed that.
Then in my Repository:
Page<Object[]> listAccessibleInstances(
#Param("username")final String username,
#Param("instanceStates") final Set<String> instanceStates,
final Pageable pageable);
Thats it! Now the result of my case statement is returned with each entity.
Object[0] = original, default mapped entity.
Object[1] = permission

#ManyToMany after save of one entity list is refreshed after save of second isn't

Hello everybody my problem is pretty strange so let me show you part of code:
Part of AllegroFieldImpl:
#ManyToMany(targetEntity = AllegroCategoryImpl.class)
#JoinTable(name = "ALLEGRO_CATEGORIES_XREF", joinColumns = {
#JoinColumn(name = "ALLEGRO_FIELD_ID", nullable = false, updatable = false) },
inverseJoinColumns = { #JoinColumn(name = "ALLEGRO_CATEGORY_ID",
nullable = false, updatable = false) })
#JsonBackReference
private List<AllegroCategory> allegroCategory = new ArrayList<AllegroCategory>();
Part of AllegroCategoryImpl:
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL,targetEntity = AllegroFieldImpl.class)
#JoinTable(name = "ALLEGRO_CATEGORIES_XREF", joinColumns = {
#JoinColumn(name = "ALLEGRO_CATEGORY_ID", nullable = false, updatable = false) },
inverseJoinColumns = { #JoinColumn(name = "ALLEGRO_FIELD_ID",
nullable = false, updatable = false) })
private List<AllegroField> allegroFields = new ArrayList<AllegroField>();
As you can see I have here 2 entities with ManyToMany relations model. Everything is working pretty good the lists are fetched etc. but there is one problem that I can't pass. When I create new AllegroFieldImpl and add for it existing AllegroCategoryImpl everything is saving on database well but when I execute method in AllegroCategoryImpl getAllegroFields() its looks like there is no update (I must restart server - maybe something is stored in catche ?). Here is code sample that will explain you all:
AllegroCategory ac = allegroService.readAllegroCategoryByID(1);
logger.error("Fields size: "+ac.getAllegroFields().size()); // 5
// Creating new AllegroField
AllegroField afnew = new AllegroFieldImpl();
afnew.setAllegroFieldId(421312);
afnew.setName("asdasdasdsa12312312312");
afnew.setObligatory(false);
afnew.getAllegroCategory().add(ac);
em.merge(afnew);
// Checking fields size
logger.error("Fields size: "+ac.getAllegroFields().size()); // 5
Any ideas how to solve this problem ?
Check if method have readAllegroCategoryByID - #Transactional

JPA, merge objects updating childrens

I´ve got this on the parent object
#OneToMany(mappedBy="idUser", cascade = CascadeType.MERGE)
public List<Directions> directions;
And in my controller I´ve got this
public static void userUpdate(String apikey, JsonObject body) {
if(validate(apikey)) {
Long idUser = decode(apikey);
User oldUser = User.findById(idUser);
Map<String, User> userMap = new HashMap<String, User>();
Type arrayListType = new TypeToken<Map<String, User>>(){}.getType();
userMap = gson().fromJson(body, arrayListType);
User user = userMap.get("user");
oldUser.em().merge(user);
oldUser.save();
}else{
forbidden();
}
}
It makes the update on the parent object but when I change something on the children object it doesn't update it and neither gives problems with hibernate or Oracle.
Does anyone know why it doesn´t update the child object?
Thanks all!
Updated with solution!
This is how it works for me, as #JB Nizet said you´ve got to save the child objects too.
oldUser.em().merge(user);
oldUser.save();
for (Direction direction : oldUser.directions) {
direction.save();
}
Another aproach!
With this
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "SASHNBR", insertable = true, updatable = true)
public List<Direction> directions;
I´ve been able to make oldUser.save() and get the child objects saved.
AFAIK, Play requires a call to save() on all the modified entities. So you probably need to iterate through the user's directions and save them as well:
for (Direction direction : user.getDirections()) {
direction.save();
}

Resources