DuplicateKeyException: Bulk write operation error on server: - spring

I am new in MongoDB and trying to execute a very simple query to save collection to the database, but receive the error Bulk write operation error on server.
Entity:
#Document("role")
#Data
#NoArgsConstructor
#RequiredArgsConstructor
#EqualsAndHashCode(exclude = "id") <--------- compare only by unique field `name`
public class Role {
#Id
private String id;
#NotBlank
#NonNull
#Indexed(unique = true)
private ERole name;
}
ERole:
public enum ERole {ROLE_USER, ROLE_MODERATOR, ROLE_ADMIN}
logic:
Collection<Role> rolesFromDb = repository.findAll(); < --------- (1)
Collection<Role> rolesFromEnumSet = Arrays.stream(ERole.values()).map(Role::new).collect(Collectors.toSet());
if (!(rolesFromDb.containsAll(rolesFromEnumSet) &&
rolesFromEnumSet.containsAll(rolesFromDb))
) {
rolesFromEnumSet.removeAll(rolesFromDb); < ------------------ (2)
repository.saveAll(rolesFromEnumSet); < --------------------- (3)
}
I have a single record in the database and receive the next collection in rolesFromDatabase variable (line (1):
Role(id=63dc16b253565a43cee65848, name=ROLE_USER)
In line (2) a rolesFromEnuSet has an expected set:
0 = {Role#9387} "Role(id=null, name=ROLE_ADMIN)"
1 = {Role#9388} "Role(id=null, name=ROLE_MODERATOR)"
So, the database doesn't contain remembered records.
Why do I receive a "Duplicate error" in line (3)?

Your EqualsAndHashCode is comparing by ID, not name. This means that the rolesFromEnumSet.removeAll(rolesFromDb)) code is not removing anything in rolesFromEnumSet
This means that inside your transaction, you are actually left with 4 records:
Role(id=63dc16b253565a43cee65848, name=ROLE_USER)
Role(id=null, name=ROLE_USER)
Role(id=null, name=ROLE_MODERATOR)
Role(id=null, name=ROLE_ADMIN)
You'll receive a duplicate key error because it tries to save an additional role with the name ROLE_USER

Related

java jpa SqlResultSetMapping issue

I have a table form_header with 3 records
There are more fields in the table decided not to add it here in the post since most are irrelevant. I created a class/entity to get the count with distinct for each status in sql.
#Entity()
#Table(name = "Form_Header")
#SqlResultSetMapping(name = "myMapping",
entities = {#EntityResult(
entityClass = FormSummary.class,
fields = {#FieldResult(name = "status", column = "status"),
#FieldResult(name = "id", column = "header_id")})})
public class FormSummary {
#Id()
private Long id;
private String status;
<getter and setter>
with entity manager
List<FormSummary> results = entityManager.createNativeQuery("select DISTINCT(status), COUNT(header_id) as header_id from Form_Header where is_deleted = 0 group by status order by status", "myMapping").getResultList();
for (FormSummary x : results) {
System.out.println("ABC " + x.getId());
System.out.println("ABC " + x.getStatus());
}
Issue is the sysout is showing this
instead of this
status header_id
APPROVE 1
DRAFT 1
SUBMITTED 1
Whats even weird is if I add an extra record in the table with the same status
I will get the correct data in my jpa
Am I missing something in my code or a possible bug with SqlResultSetMapping?

Spring's findByColumnName returning empty list

I need to retrieve a list of Category from the DB on the basis of value of column called owner. Here is my Category -
#Entity
#Table(name = "categories")
class Category(#Column(name = "category_id", nullable = false)
#Id #GeneratedValue(strategyGenerationType.AUTO)
var id: Long = 0,
#Column(name = "category_owner", nullable = false)
#field:NotNull(message = "Please assign an owner")
var owner: Long?,
#Column(name = "category_name", nullable = false)
#field:NotEmpty(message = "Please assign a name")
var name: String?)
Here is my interface which defines the function findByOwner -
interface CategoryRepository: JpaRepository<Category, Long> {
fun findByOwner(categoryOwner: Long): List<Category>
}
However, when I call the method, I get no response. I have made sure that the DB has correct data and I'm providing the correct owner Id. Have even invalidated the cache etc. What could be going wrong?
EDIT:
After spring.jpa.show-sql=true -
findAll()
Hibernate: select category0_.category_id as category1_0_, category0_.category_name as category2_0_, category0_.category_owner as category3_0_ from categories category0_
findByOwner()
Hibernate: select category0_.category_id as category1_0_, category0_.category_name as category2_0_, category0_.category_owner as category3_0_ from categories category0_ where category0_.category_owner=?
EDIT 2:
Turns out that my implementation was fine all along. The bug was in my service.
Create your named method according with the name of the column.
fun findByCategoryOwner(categoryOwner: Long): List<Category>
Or use #Query
#Query("SELECT * FROM categories WHERE category_owner = ?1", nativeQuery = true)
fun findByOwner(cateogryOwner: Long): List<Category
Can you put a breakpoint in org.springframework.data.jpa.repository.query.JpaQueryExecution class and when you execute findByOwner, it will come here.
When it reaches this breakpoint, select the query.createQuery(accessor).getResultList() and evaluate to see what value is returned by hibernate for spring-data-jpa to use
This post should help you. It appears to be happeing because of the parameter name mismatch.
Use camelCase to name your variables in Entity class then jpa will auto recognise the column name
findByCategoryOwner(String categoryOwner)
If you still wish to have underscore in your column names then try this
findByCategory_Owner(String categoryOwner)
I haven't tried the second option though
At least in java you need to provide the id in the method name:
**fun findByOwner_Id(categoryOwner: Long): List<Category>**
So change it from findByOwner -> findByOwnerId.

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".

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

JPA - How to select from a database function?

I have the following SQL query in T-SQL:
SELECT a.ReportDate,
a.Value,
a.Quantity,
a.ID,
a.Code
FROM AQF.fCalc(#data) a
#data is a varbinary. I'm trying to replicate this query using JPA. (I'm actually using Spring Data JPA 1.10.2.) I have the following defined in MyRepository:
#Query("SELECT a "
+ "FROM function('AQF.fCalc', :data) a "
)
List<MyClass> getCalcData(#Param("data") byte[] data);
MyClass is as follows:
#Entity
#Data // using lombok to create setters, getters, etc.
public class MyClass {
#Id
private Long id;
private Date reportDate;
private BigDecimal value;
private BigDecimal quantity;
private String code;
}
When I call getCalcData, I receive the following error:
antlr.NoViableAltException: unexpected token: function
at org.hibernate.hql.internal.antlr.HqlBaseParser.fromRange(HqlBaseParser.java:1501) [hibernate-core-5.0.9.Final.jar:5.0.9.Final]...
How can I call the function AQF.fCalc (and pass in the byte array) using JPA?
Update 1
I've now tried using a native query too, but now I get an error stating:
java.sql.SQLException: An error occurred while getting new row from user defined Table Valued Function :
System.Xml.XmlException: Unexpected end of file has occurred. The following elements are not closed: Data, Report. Line 24, position 804.
Here's the test code for the native query in MyRepository:
#Query(value = "SELECT a.code "
+ "FROM AQF.fCalc(?1) a",
nativeQuery = true)
List<String> getCalcData(byte[] data);
Update 2
I was able to resolve the error noted in "Update 1" above. The byte array of data in the getCalcData was corrupted. Once I resolved that issue, the error no longer appeared. So, now I can run as a native query. However, I would still like to know how to use JPQL to make this function call.
You can use StoredProcedureQuery. From the javadoc
Interface used to control stored procedure query execution.
Refer this sample code.
Assume your SQL function name is sales_tax in this example.
EntityManager em = factory.createEntityManager();
// Create call stored procedure
em.getTransaction().begin();
StoredProcedureQuery storedProcedure = em.createStoredProcedureQuery("sales_tax");
// set parameters
storedProcedure.registerStoredProcedureParameter("subtotal", Double.class, ParameterMode.IN);
storedProcedure.registerStoredProcedureParameter("tax", Double.class, ParameterMode.OUT);
storedProcedure.setParameter("subtotal", 1f);
// execute SP
storedProcedure.execute();
// get result
Double tax = (Double)storedProcedure.getOutputParameterValue("tax");
System.out.println("Tax is: " + tax);
em.getTransaction().commit();
em.close();

Resources