Hibernate Search 6 search multiple fields with multiple keywords - spring-boot

I want to search through these 2 different variables :
#Enumerated(EnumType.STRING)
#Column(length = 20)
#Convert(converter = WorksEnumConverter.class)
#GenericField(valueBridge = #ValueBridgeRef(type = WorksValueBridge.class))
private WorksEnum works;
#Convert(converter = AcquisitionTypeConverter.class)
#Enumerated(EnumType.STRING)
#Column(length = 10)
#GenericField(valueBridge = #ValueBridgeRef(type = AcquisitionTypeBridge.class))
private AcquisitionTypeEnum acquisitionType;
As you can see there are 2 types of Enum , and i want to use hibernate search to search via multiple keywords, I used bridge and converter and i always get the error of cannot convert string to enum
This is my code for hibernate search implementation :
if (requestSearchCustomer.getKeyword() != null) {
final String[] keywords = requestSearchCustomer.getKeyword().split(",");
final SearchPredicate keywordPredicate = getSearchScope().predicate().terms()
.fields(RequestDB_.WORKS, RequestDB_.ACQUISITION_TYPE)
.matchingAny(keywords).toPredicate();
predicate.must(keywordPredicate);
}

If you want to pass strings, and not the enum type that Hibernate Search expects, you will need to disable Hibernate Search's automatic conversion:
if (requestSearchCustomer.getKeyword() != null) {
final String[] keywords = requestSearchCustomer.getKeyword().split(",");
final SearchPredicate keywordPredicate = getSearchScope().predicate().terms()
.fields(RequestDB_.WORKS, RequestDB_.ACQUISITION_TYPE)
.matchingAny(Arrays.asList(keywords), ValueConvert.NO).toPredicate();
predicate.must(keywordPredicate);
}
See https://docs.jboss.org/hibernate/stable/search/reference/en-US/html_single/#search-dsl-argument-type

Related

Inconsistent Owned Hibernate Object

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?

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

Find list with entries which have max version of this entries

I have some entities which uses a slowly changing versioning by version-number without a current-version-flag. My entity looks like:
#Entity
public class MyEntity {
#Id
private long id;
private long functionalKey;
private String name;
private int version;
}
There are entries like:
{ id = 1, functionalKey = 1, name = "Test1", version = 1 }
{ id = 2, functionalKey = 18, name = "Test2", version = 1 }
{ id = 32, functionalKey = 18, name = "New Test 2", version = 2 }
How can I use Spring data to find all entries (by functionalKey) with the highest version number?
To find a single entry I can use something like MyEntity findFirstByFunctionalKeyOrderByVersionDesc(long functionalKey) or write a suitable expression statement. But I want all entities not only one! I search for something like List<MyEntity> findAllWithHighestVersionGroupedByFunctionalKey.
How is this possible?
Thanks for your support!
You cannot do that. Here is the list of things you can achieve with query methods https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation . You should use JPQL.

How to get an array of objects with spring #RequestParam in ManyToMany relationship?

I have an entity Movies and an entity Genres in a relationship many to many.
#ManyToMany
#JoinTable(name = "movies_genres", joinColumns = #JoinColumn(name = "movies_id"), inverseJoinColumns = #JoinColumn(name = "genres_id"))
private Set<Genres> genresSet = new HashSet<>();
In a rest controller class, I want to do this:
#GetMapping("/search-movies")
public Iterable<Movies> search(
#RequestParam(value = "genresSet", required = false) Set<Genres> genresSet,
#RequestParam(value = "synopsis", required = false) String synopsis,
#RequestParam(value = "title", required = false) String title,
#RequestParam(value = "runtime", required = false) Integer runtime
)
I use axios on front-end to send params to the back-end and genresSet is array of objects, for example
[
{ id: 1, name: 'action'},
{ id: 2, name: 'crime'},
{ id: 3, name: 'comedy'}
]
I thought that Spring would automatically convert array of objects into set of genres, but it gives me null.
How to get values of genres in form of a set of values?
To recap, the user enters more than one genre, where each genre is represented as an object, so front-end sends array of genre objects and back-end needs to bind that array to the set of genres, where set of genres is a many to many property of Movie entity.
You can rethink your parameters and change Set<Genres> either to Set<Integer> or Set<String>. Because you need just ids or just names of genres to use them in search query, you don't need to pass the whole Genres object.
#GetMapping("/search-movies")
public Iterable<Movies> search(
#RequestParam(value = "genresIds", required = false) Set<Long> genresIds,
// ... other parameters without changes
would accept
"genresIds": [1, 2, 3]
or
#GetMapping("/search-movies")
public Iterable<Movies> search(
#RequestParam(value = "genresNames", required = false) Set<String> genresNames,
// ... other parameters without changes
would accept
"genresNames": ['action', 'crime', 'comedy']
Another way you could accomplish this is to create a wrapper class for the Set. For example:
public class GenresWrapper {
Set<Genres> genresSet;
// ... accessors
}
Then your controller method would look like this:
#GetMapping("/search-movies")
public Iterable<Movies> search(
#ModelAttribute GenresWrapper genres,
// ... other parameters
)
Or you could wrap all the request parameters in a single wrapper object.
This answer is based on another SO answer.

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

Resources