Can we use Composite Primary Key Mapping in spring data elastic search - java

I have an entity 'Product' and I want the primary key in ES to be used as a combination of 'id' and 'name' attributes. How can we do that using spring data elastic search.
public class Product {
#Id
private String id;
#Id
private String name;
#Field(type = FieldType.Keyword)
private Category category;
#Field(type = FieldType.Long)
private double price;
#Field(type = FieldType.Object)
private List<ValidAge> age;
public enum Category {
CLOTHES,
ELECTRONICS,
GAMES;
}
}

One way to achieve this would be the following:
first rename your id property, I changed it to documentId here. This is necessary, because in Spring Data
Elasticsearch an id-property can be either annotated with #Id or it can be namend id. As there can only be one
id-property we need to get this out of the way. It can have the name id in Elasticsearch, set by the #Field
annotation, but the Java property must be changed.
second, add a method annotated with #Id and #AccessType(AccessType.Type.PROPERTY) which returns the value you
want to use in Elasticsearch.
third, you need to provide noop-setter for this property. This is necessary because Spring Data Elasticsearchsoe
not check the id property to be read only when populating an entity after save or when reading from the index.
This is a bug in Spring Data Elasticsearch, I'll create an issue for that
So that comes up with an entity like this:
#Document(indexName = "composite-entity")
public class CompositeEntity {
#Field(name="id", type = FieldType.Keyword)
private String documentId;
#Field(type = FieldType.Keyword)
private String name;
#Field(type = FieldType.Text)
private String text;
#Id
#AccessType(AccessType.Type.PROPERTY)
public String getElasticsearchId() {
return documentId + '-' + name;
}
public void setElasticsearchId(String ignored) {
}
// other getter and setter
}
The repository definition would be straight forward:
public interface CompositeRepository extends ElasticsearchRepository<CompositeEntity,
String> {
}
Remember that for every method that needs an Elasticsearch Id, you'll need to create like it's done in the entity
class.

I am not sure about spring data elasticsearch but spring jpa provides the facility of defining composite primary key by using #IdClass where we can define a separate class(let us say class A) in which we can define all the fields which we want to be a part of composite key Then we can use #IdClass(A.class) in entity class and use #Id annotation on all the fields which should be the part of the composite key
you can refer to this article, although I am not sure whether the same concept will be applicable for spring data es - https://www.baeldung.com/jpa-composite-primary-keys

Related

Elasticsearch and spring data with empty index

I have a question about elasticsearch with spring data.
#Data
#NoArgsConstructor
#AllArgsConstructor
#Document(indexName = "my_es_index")
public class MyEsIndex {
private String id;
private Long counter;
private Long timestamp;
}
and repository
public interface MyEsIndexRepository extends ElasticsearchRepository<MyEsIndex, String> {
Optional<MyEsIndex> findFirstByIdOrderByTimestampDesc(String id);
}
so I have a service where I have to search first for previous one saved record to retrieve previous value, always doing search ordered by timestamp.
#Service
#RequiredArgsConstructor
public class MyEsService {
private final MyEsIndexRepository repository;
public MyEsIndex insert(String previousId) {
Long previousCounter =
repository.findFirstByIdOrderByTimestampDesc(previousId).map(MyEsIndex::getCounter).orElse(0L);
var index = new MyEsIndex(UUID.randomUUID().toString(), ++previousCounter,
Instant.now().toEpochMilli());
return repository.save(index);
}
}
and when trying to do the operation receiving
{"error":{"root_cause":[{"type":"query_shard_exception","reason":"No mapping found for [timestamp] in order to sort on","index":"my_es_index"}
is it possible to do initialization for fields in elasticsearch on empty index?
because the solution of init config is not that clear because it will be used only once when starting working with empty index where never saved a record
#Configuration
public class InitElasticsearchConfig {
private final MyEsIndexRepository repository;
#EventListener(ApplicationReadyEvent.class)
public void initIndex() {
if (repository.findAll(PageRequest.of(0, 1)).isEmpty()) {
var initIndex = new MyEsIndex("initId", 0L, 0L);
repository.save(initIndex);
repository.delete(initIndex);
}
}
is it possible to delegate this solution to spring? I didn't find any
When using Spring Data Elasticsearch repositories - as you do - the normal behaviour is that the mapping is written to Elasticsearch after index creation on application startup when the index does not yet exist.
The problem in your code is that you do not define to what types the properties of your entity should be mapped; you need to add #Field annotations to do that:
#Document(indexName = "my_es_index")
public class MyEsIndex {
private String id;
#Field(type = FieldType.Long)
private Long counter;
#Field(type = FieldType.Long)
private Long timestamp;
}
Properties that are not annotated with a #Field annotation are not written to the mapping but left for automatic mapping by Elasticsearch, that's the cause for the sort not working. As there is no document written to the index, Elasticsearch does not know what type it is and how to sort on that.
In your code there is another thing that might probably not match your desired application logic. In Spring Data Elasticsearch an entity needs to have an id property, that's the property that will be used as the document's id in Elasticsearch. This is normally defined by annotating the property with #Id, if that is missing - as in your case - a property with the name of "id" or "document" is used. So in your case the property id is used.
A document's id is unique in Elasticsearch, if you store a new document under an existing id, the previous content will be overwritten. If that's what you want, the you should add the #Id annotation to your property to make it clear that this is the unique id. But in this case then your code findFirstByIdOrderByTimestamp does not make sense, as a find by id will always return at most one document, so the order by is irrelevant, you could just use a findById() then. I assume that the id should be unique as you initialize it with a UUID.
If your id is not unique and you have multiple documents with the same id and different timestamps, the you'll need to add a new unique property to your entity and annotate that with #Id to prevent id to be used as a unique identifier.

Spring Data MongoDB Unique Embedded Fields

So, I have one #Document class which has a embedded pojo field which I want it to be unique for the document based on a key in the pojo class. I tried using #CompoundIndex & #Indexed to mark it as unique but it doesn't seem to work.
#Document
public class Project {
private String id;
private String name;
private List<Details> details = new ArrayList<>();
}
public class Details{
private String key;
private String description;
}
What I want to achieve is that a project document should have unique details field in it with it's key being unique. But when I have the
#CompoundIndexes({ #CompoundIndex(name = "details_key", def = "{'details.key':1}", unique = true) }) on the Project class it doesn't work. Which I thought it should. Or am I wrong somewhere with my understanding. As I am new to this.

Spring Data JPA select sequence for different bases, and set value to entity

I need implement next logic:
I have entity:
#Data
#Entity
#Table(name = "USERS")
public class User {
#Id
#Column(name = "GUID")
private String guid;
#Column(name = "MESSAGE_ID")
private String messageId;
#Column(name = "SOME_VALUE")
private String someValue;
And I need set to someValue generated value consisting of
"some prefix"+sequencefrom DB + "some suffix";
I can make select sequense from Db, generate vsomeValue and set it to entity, but maby Is there a way to make it easier? Because in my version I use two bases, and I have to write two native query for select a sequence and use the appropriate one depending on the profile.
I need somthig like this:
#Column(name = "SOME_VALUE")
#Value(MyGenerator.class)
private String someValue;
and in MyGenerator.class implement logic for generate someValue from prefix, sequence and suffix.
Instead of annotating the class members, annotate the getters and setters, and put your logic there.
Further reference in this question.
You are looking for Custom Sequence based ID generator.
This is a nice article on it which might help you.

Map one value to multiple columns in JPA

I get an input value from the user and store it into the DB.
I use JPA to write into DB.
If I need to store the input value into 2 columns in the DB, what is the annotation I must use?
Code below.
I need to store user input - (stored in Bean field UserOrderDetails.noOfItemsSelected) into 2 columns in the DB table NoOfItemsSelected and NoOfItemsDispatched.
What annotation should I use?
The catch is that I do not want to add a field for NoOfItemsDispatched in the Entity Bean.
DB Table:
create table UserOrderDetails(
TransactionId varchar(255),
CreationDateTime datetime2(),
NoOfItemsSelected int,
NoOfItemsDispatched int
)
GO
My Entity Bean:
#Entity
#Table(name="UserOrderDetails")
public class UserOrderDetails {
#Id
private String transactionId;
private Timestamp CreationDateTime;
private int noOfItemsSelected;
//Getters and Setters
}
Controller class for Reference:
class UserOrderCreationController {
#RequestMapping(value = "/createUserOrder", method = RequestMethod.POST)
public ResponseEntity<?> createUserOrder(#RequestBody UserOrderDetails userOrderDetails , #RequestHeader HttpHeaders headers) throws Exception{
//Some business logic to handle the user Order
dbrepository.save(UserOrderDetails);
return new ResponseEntity<UserOrderDetails>(userOrderDetails, HttpStatus.CREATED);
}
}
Thanks
As far as i know you just need to add annotations as below on the fields in the DAO layer entity class.if you have the same field names and column names these annotations are optional.
As the question states one value to multiple columns,before the the DAO.save is called you should be populating the value in both the fields.
#Entity
#Table(name="UserOrderDetails")
public class UserOrderDetails {
//Need to have identity generator of your wish
#Id
#Column(name = "TRANSACTION_ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private String transactionId;
private Timestamp CreationDateTime;
**#Column(name = "NO_OF_ITEMS_SELECTED")**
private int noOfItemsSelected;
**#Column(name = "NO_OF_ITEMS_DISP")**
private int noOfItemsDispatched;
//Getters and Setters
}
OR
Alternatively we can use Hibernate's #Formula annotation.Populate the one field which is coming from user request to the corresponding field and For the other column for which you can keep the same value by using #Formula.Look for some examples of #Formula
#Formula("noOfItemsSelected")
private float noOfItemsDispatched;

How to get a unique key for two fields with Hibernate?

I have two fields of an entity class which I don't want to be unique but to instead be used as composite fields for a key which must itself be unique. For example I have two fields (name and version) which can be the same for other records but together they must be unique. What is the best way to do that using Hibernate (with annotations)? I am using Hibernate Validator for other fields but I am not sure of a way to use that to validate that two fields together compose a unique key. I am using a generic entity class which has an id generic type which can be swapped out for a composite key class but I have yet to get that to work very well.
This will create a unique key on the database:
#Table( name = "MYTABLE",
uniqueConstraints = { #UniqueConstraint( columnNames = { "NAME", "VERSION" } ) } )
This will be enforced by the database on a update or persist.
You'd need to write your own custom validator if you wanted to enforce this using Hibernate Validator.
We usually will wrap the two fields in an inner key class which is marked as #Embeddable. For example:
#Entity
public class Foo {
#EmbeddedId()
private Key key;
...
#Embeddable
public static class Key {
#Column(nullable=false)
private String name;
#Column(nullable=false)
private int version;
protected Key () {
// for hibernate
}
public Key (String name, int version) {
this.name = name;
this.version = version;
}
...
// You probably want .equals and .hashcode methods
}
}

Categories