How do I implement tenant-based routing for elasticsearch in JHipster? - elasticsearch

I´m currently trying to implement multi-tenancy into my JHipster microservices. However, I can't find a way to implement tenant-based routing for elasticsearch.
So far I have managed to implement datasource routing for the PostgreSQL DBs similar to the following article: https://websparrow.org/spring/spring-boot-dynamic-datasource-routing-using-abstractroutingdatasource
When I started looking for ways to implement multi tenancy in elasticsearch, I found the following article: https://techblog.bozho.net/elasticsearch-multitenancy-with-routing/
There I read about tenant-based routing. First I tried looking it up on the internet, but anything I found was either over 5 years old or not related to java, much less to Spring/Jhipster. Then I tried looking into the methods of ElasticSearchTemplate, the annotation variables of #Document and #Settings and the configuration options in the .yml file, but didn't find anything useful.
I'm currently using Jhipster version 7.9.3, which uses the Spring-Boot version 2.7.3. All the microservices were created with JDL and on half of them I put elasticsearch into the configuration. The other half does not matter.
Edit: I want to add that multi-tenancy in my database is archived by database separation(Tenant1 uses DB1, Tenant2 uses DB2 etc.). The tenant variable is an enum and not included in my entities.
Edit2: I implemented my own solution. I use the tenants as indexes and use my ContextHolder from DataSource Routing to route to the correct tenant index. For that I had to do some changes the elasticsearchTemplate in the generated classes of the package "<my.package.name>.repository.search".
It might not be the most efficient way to reach multi tenancy with elasticsearch, but it doesn't need much configuration.
Here is the code:
public interface ProductSearchRepository extends ElasticsearchRepository<Product, Long>, ProductSearchRepositoryInternal {}
interface ProductSearchRepositoryInternal {
Stream<Product> search(String query);
Stream<Product> search(Query query);
void index(Product entity);
}
class ProductSearchRepositoryInternalImpl implements ProductSearchRepositoryInternal {
private final ElasticsearchRestTemplate elasticsearchTemplate;
private final ProductRepository repository;
ProductSearchRepositoryInternalImpl(ElasticsearchRestTemplate elasticsearchTemplate, ProductRepository repository) {
this.elasticsearchTemplate = elasticsearchTemplate;
this.repository = repository;
}
#Override
public Stream<Product> search(String query) {
NativeSearchQuery nativeSearchQuery = new NativeSearchQuery(queryStringQuery(query));
return search(nativeSearchQuery);
}
#Override
public Stream<Product> search(Query query) {
return elasticsearchTemplate.search(query, Product.class, IndexCoordinates.of(TenantContextHolder.getTenantContext().getTenant())).map(SearchHit::getContent).stream();
}
#Override
public void index(Product entity) {
repository.findById(entity.getId()).ifPresent(t -> elasticsearchTemplate.save(t, IndexCoordinates.of(TenantContextHolder.getTenantContext().getTenant())));
}
}
Edit3: Since people might not know where ".getTenant()" comes from, I'll show my tenant enumeration:
public enum Tenant {
TENANTA("tenant_a"),
TENANTB("tenant_b");
String tenant;
Tenant(String name) {
this.tenant=name;
}
public String getTenant() {
return this.tenant;
}
}
Edit4: My solution is not working as planned. I will give an update once I found a better and more robust solution.
Edit5: I have found out how to implement tenant-based routing. First you have to add the following Annotation to your entities:
#org.springframework.data.elasticsearch.annotations.Routing(value = "tenant")
In my case I had to include the enum "Tenant" into my entities along with the getter and setter:
#Transient
private Tenant tenant;
public Tenant getTenant() {
return tenant;
}
public void setTenant(Tenant tenant) {
this.tenant = tenant;
}
Then I have to set the tenant during the processing of a REST request before it gets indexed by elasticsearchtemplate:
entity.setTenant(TenantContextHolder.getTenantContext());
As for the search function, I had to add a term query as a filter to enable routing:
#Override
public Stream<Product> search(String query) {
NativeSearchQuery nativeSearchQuery = new NativeSearchQuery(queryStringQuery(query)
, QueryBuilders.termQuery("_routing", TenantContextHolder.getTenantContext()));
return search(nativeSearchQuery);
}
The method "setRoute(String route)" of "nativeSearchQuery" either does not work in my case or I didn't understand how it works.
I have successfully tested this implementation with GET and POST requests. Currently I have a problem with elasticsearch overwriting data if the id of the entity from one tenant I want to save is the same id as another entity with a different tenant.

After some trial and error, I found a solution to the overwriting problem and successfully completed and tested my implementation of tenant-based routing. Here is the code:
Product.java
import java.io.Serializable;
import javax.persistence.*;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.springframework.data.elasticsearch.annotations.Field;
#Entity
#Table(name = "product")
#Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
#org.springframework.data.elasticsearch.annotations.Document(indexName = "product")
#SuppressWarnings("common-java:DuplicatedBlocks")
#org.springframework.data.elasticsearch.annotations.Routing(value = "tenant")
public class Product implements Serializable {
private static final long serialVersionUID = 1L;
#Transient
private Tenant tenant;
#Transient
#Field(name = "elastic_id")
#org.springframework.data.annotation.Id
private String elasticsearchId;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator =
"sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
#Column(name = "id")
#Field("postgres_id")
private Long id;
//Getters, Setters and other variables
}
ProductSearchRepository
public interface ProductSearchRepository extends ElasticsearchRepository<Product, Long>, ProductSearchRepositoryInternal {}
interface ProductSearchRepositoryInternal {
Stream<Product> search(String query);
Stream<Product> search(Query query);
void index(Product entity);
Product save(Product entity);
void deleteById(Long id);
}
#Transactional
class ProductSearchRepositoryInternalImpl implements ProductSearchRepositoryInternal {
private final ElasticsearchRestTemplate elasticsearchTemplate;
private final ProductRepository repository;
ProductSearchRepositoryInternalImpl(ElasticsearchRestTemplate elasticsearchTemplate, ProductRepository repository) {
this.elasticsearchTemplate = elasticsearchTemplate;
this.repository = repository;
}
#Override
public Stream<Product> search(String query) {
NativeSearchQuery nativeSearchQuery = new NativeSearchQuery(queryStringQuery(query)
, QueryBuilders.termQuery("_routing", TenantContextHolder.getTenantContext()));
nativeSearchQuery.setMaxResults(30);
return search(nativeSearchQuery);
}
#Override
public Stream<Product> search(Query query) {
return elasticsearchTemplate.search(query, Product.class).map(SearchHit::getContent).stream();
}
#Override
public void index(Product entity) {
entity.setTenant(TenantContextHolder.getTenantContext());
repository.findById(Long.valueOf(entity.getId())).ifPresent(t -> {
entity.setElasticsearchId(entity.getTenant()+String.valueOf(entity.getId()));
elasticsearchTemplate.save(t);
});
}
#Override
public Product save(Product entity) {
entity.setTenant(TenantContextHolder.getTenantContext());
entity.setElasticsearchId(entity.getTenant()+String.valueOf(entity.getId()));
return elasticsearchTemplate.save(entity);
}
#Override
public void deleteById(Long id) {
elasticsearchTemplate.delete(TenantContextHolder.getTenantContext() + String.valueOf(id), Product.class);
}
}

Related

Jackson: Multiple Serializers on the same entity when differents Rest EndPoint are called

I'm trying to avoid using the DTO antipattern when different EndPoint are called, where each returns a distinct representation of the same entity. I'd like to take advantage of the serialization that Jackson performs when I return the entity in the Rest EndPoint. This means that serialization is only done once and not twice as it would be with a DTO (entity to DTO and DTO to Json):
EndPoints example:
#GetMapping("/events")
public ResponseEntity<List<Event>> getAllEvents(){
try {
List<Event> events = (List<Event>) eventsRepository.findAll();
return new ResponseEntity<List<Event>>(
events, HttpStatus.OK);
}catch(IllegalArgumentException e) {
return new ResponseEntity<List<Event>>(HttpStatus.BAD_REQUEST);
}
}
#GetMapping("/events/{code}")
public ResponseEntity<Event> retrieveEvent(#PathVariable String code){
Optional<Event> event = eventsRepository.findByCode(code);
return event.isPresent() ?
new ResponseEntity<Event>(event.get(), HttpStatus.OK) :
new ResponseEntity<Event>(HttpStatus.BAD_REQUEST);
}
Serializer (class that extends of StdSerializer):
#Override
public void serialize(Event value, JsonGenerator gen,
SerializerProvider provider) throws IOException {
if(firstRepresentation) {
//First Representation
gen.writeStartObject();
gen.writeNumberField("id", value.getId());
gen.writeObjectField("creation", value.getCreation());
gen.writeObjectFieldStart("event_tracks");
for (EventTrack eventTrack : value.getEventsTracks()) {
gen.writeNumberField("id", eventTrack.getId());
gen.writeObjectField("startTime", eventTrack.getStartTime());
gen.writeObjectField("endTime", eventTrack.getEndTime());
gen.writeNumberField("priority", eventTrack.getPriority());
gen.writeObjectFieldStart("user");
gen.writeNumberField("id", eventTrack.getUser().getId());
gen.writeEndObject();
gen.writeObjectFieldStart("state");
gen.writeNumberField("id", eventTrack.getState().getId());
gen.writeStringField("name", eventTrack.getState().getName());
gen.writeEndObject();
}
gen.writeEndObject();
gen.writeEndObject();
}else if(secondRepresentation) {
//Second Representation
}
}
Entity:
#JsonSerialize(using = EventSerializer.class)
#RequiredArgsConstructor
#Getter
#Setter
public class Event implements Comparable<Event>{
private Long id;
#JsonIgnore
private String code;
private Timestamp creation;
#NonNull
private String description;
#JsonUnwrapped
#NonNull
private EventSource eventSource;
#NonNull
private String title;
#NonNull
private Category category;
#NonNull
#JsonProperty("event_tracks")
private List<EventTrack> eventsTracks;
#JsonProperty("protocol_tracks")
private List<ProtocolTrack> protocolTracks;
public void addEventTrack(#NonNull EventTrack eventTracks) {
eventsTracks.add(eventTracks);
}
#JsonIgnore
public EventTrack getLastEventTrack() {
return eventsTracks.get(eventsTracks.size() - 1);
}
#JsonIgnore
public int getLastPriority() {
return getLastEventTrack().getPriority();
}
public void generateUUIDCode() {
this.code = UUID.randomUUID().toString();
}
#Override
public int compareTo(Event o) {
return this.getLastPriority() - o.getLastPriority();
}
}
So, so far I have been able to serialize a representation type with a class that extend of StdDeserializer, but this doesn't give me the flexibility to extend the representations of the same entity attributes in multiple ways. Although I've tried it with Json annotations, but I realize that the more representations the entity class has, it can get very complex, something that it should be simple. Maybe some idea how I could do it.
Thank you.
If you want to define multiple representations of the same bean you could use Jackson JsonView.
With json views you can set different strategies to define which property will be serialized in the response and so use different views by endpoint.
Documentation here : https://www.baeldung.com/jackson-json-view-annotation
Just don't forget that you doing REST here....avoid expose too many representations of the same resource

Spring boot + DocumentDB custom Query for searching documents

I have Spring Boot + DocumentDB application in which we need to implement a search API, The Search fields are part of nested Json are as below:
{
"id":"asdf123a",
"name":"XYZ",
add{
"city":"ABC"
"pin":123456,
}
}
I need to search with name="XYZ" and city="ABC", I'm trying with below code but somehow not able to retrieve the record.
But I'm able to retrieve with just by Name or ID, but not with name and city or just city which is part of nested JSON.
Employee{
private String id;
private String name;
private Address add
/*Getter and Setters {} */
}
Address{
private String city;
private Long pin;
/*Getter and Setters {} */
}
public class EmployeeController {
EmployeeRepository repository;
#Autowire
EmployeeController (EmployeeRepository repository){
this.repository = repository;
}
#GetMapping(value = "/search", produces = APPLICATION_JSON_VALUE_WITH_UTF8)
public ResponseEntity<?> Search (#RequestParam ("name")String name,
#RequestParam("city") String city){
return new ResponseEntity <> (repository
.findByNameLikeAndAddressCityLike(
name, city),
HttpStatus.OK
);
}
}
#Repository
public interface EmployeeRepository extends DocumentDbRepository<Employee,
String> {
Optional<Employee>findByNameLike(String name); // Perfectly working
Optional<Employee>findByAddressCityLike(String city); // Not working
Optional<Employee>findByNameLikeAndAddressCityLike(String name, String
city); // Not Working
}
Also Just like Spring JPA we use #Query to fire custom/ native query are there any DocumentDB Annotation present if so please guide me with example or Docuemnt. Looking for help

Relationships breaking with spring-data-neo4j 4.0.0

I've got something weird in my project and I can't see if it's may fault or not.
I'm using Spring with spring-data-neo4j (v4.0.0).
This code in my controller breaks relationships.
#RequestMapping("/initialize")
public Object initialize() {
Organization orga = new Organization();
orga.setName("WEAVERS");
this.organizationRepository.save(orga);
User user = new User();
user.setEmail("t.rignoux#gmail.com");
user.setFirst_name("Thierno");
user.setLast_name("Rignoux");
user.setLogin("317");
user.setOrganization(orga);
this.userRepository.save(user);
User user2 = new User();
user2.setEmail("petitenessie#gmail.com");
user2.setFirst_name("Eléonore");
user2.setLast_name("Klein");
user2.setLogin("nessie");
user2.setOrganization(orga);
this.userRepository.save(user2);
Project project = new Project();
project.setName("PROJET 1");
project.setOrganization(orga);
project.setUser(user);
this.projectRepository.save(project);
Project project2 = new Project();
project2.setName("PROJET 2");
project2.setOrganization(orga);
project2.setUser(user2);
this.projectRepository.save(project2);
return orga;
}
As you can see on this graph:
The relation between ORGANIZATION and USER1 has been severed when I created USER2.
Broken relationships is something I see everywhere in my application... I don't understand!
neo4j configuration
#EnableTransactionManagement
#Import(RepositoryRestMvcConfiguration.class)
#EnableScheduling
#EnableAutoConfiguration
#ComponentScan(basePackages = {"fr.weavers.loom"})
#Configuration
#EnableNeo4jRepositories(basePackages = "fr.weavers.loom.repositories")
public class LoomNeo4jConfiguration extends Neo4jConfiguration {
public static final String URL = System.getenv("NEO4J_URL") != null ? System.getenv("NEO4J_URL") : "http://localhost:7474";
#Override
public Neo4jServer neo4jServer() {
return new RemoteServer(URL,"neo4j","loomREST2016");
}
#Override
public SessionFactory getSessionFactory() {
return new SessionFactory("fr.weavers.loom.domain");
}
}
Organization Domain :
public class Organization extends Node {
#GraphId
Long id;
String name;
#Relationship(type = "SET_UP", direction = Relationship.OUTGOING)
Set<SET_UP> projects;
#Relationship(type = "WORK_FOR", direction = Relationship.INCOMING)
Set<WORK_FOR> users;
[...]
WORK_FOR Domain :
public class WORK_FOR extends Relationship {
#GraphId
private Long relationshipId;
#StartNode
public User user;
#EndNode
public Organization organization;
public WORK_FOR() {
super();
}
}
User Domain:
public class User extends Node {
#GraphId
Long id;
String login;
#JsonIgnore
String password;
String first_name;
String last_name;
String email;
#Relationship(type = "WORK_FOR")
WORK_FOR workFor;
I looked everywhere and I can't find anything to help me...
Thank you
In your initialisation request you make several repository calls to construct the database. Each of these calls will construct a new Session, effectively clearing out information from the previous one because the Spring config defaults to Session-per-request. This is most likely the reason existing relationships are being deleted.
If you add the following bean definition to your config, I believe the problem should be resolved.
#Override
#Bean
#Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public Session getSession() throws Exception {
return super.getSession();
}
Please refer to the documentation here for more information about Session scope.
Please annotate the incoming relationship on your Node class:
#Relationship(type = "WORK_FOR", Direction = Relationship.INCOMING)
WORK_FOR workFor;
As per the documentation, all INCOMING relationships must be fully annotated - the default is OUTGOING.

calculated fields in Entity: Autowired is null

I have an Entity
#Entity
#Table(name = "orgtree")
public class OrganizationTree {
#Id
#Column(name="ORGANIZATION_ID")
private String organizationId;
#Column(name="ORGANIZATION_NAME")
private String organizationName;
}
and a repository to provide REST access
#RepositoryRestResource(collectionResourceRel = "organizationTree", path = "organizationTree")
public interface OrganizationTreeRepository extends JpaRepository<OrganizationTree,String> {
#Query
#RestResource(path = "findAll", rel = "findAll")
List<OrganizationTree> findAll();
}
So far so good.
Now I want to add a calculated field to my entity
#Autowired
#Transient
private OrgTreeService orgTreeService;
#JsonSerialize
public Integer getPersonCount() {
return orgTreeService.getPersonCount(organizationId);
}
Here I have several problems:
orgTreeService is null
people say that it's a bad practice to use a
service in an Entity
What is the canonical solution to this problem?
A solution I found (or should I call it a hack) is the following:
I annotate a calculated field with a custom serializer:
#Formula(value = "ORGANIZATION_ID")
#JsonSerialize(using=JsonOrgPersonCountSerializer.class)
private String personCount;
In the serializer I compute the person's count:
public class JsonOrgPersonCountSerializer extends JsonSerializer<String> {
#Override
public void serialize(String source, JsonGenerator gen, SerializerProvider prov) throws IOException, JsonProcessingException {
gen.writeString("" + orgTreeService.getPersonCount(source));
}
}
Another solution would be to use some kind of Data Transfer Object where I can call my service.

Using session in old Petclinic example

I'm experimenting with the old Petclinic example and I noticed that the vets ArrayList in the SimpleJdbcClinic exists for the life of the session. It seems like it should exist only for the request since I don't see any annotations putting it into the session context. Could someone point out what I'm failing to understand?
Here is the vets class:
#XmlRootElement
public class Vets {
private List<Vet> vets;
#XmlElement
public List<Vet> getVetList() {
if (vets == null) {
vets = new ArrayList<Vet>();
}
return vets;
}
}
The service:
#Service
#ManagedResource("petclinic:type=Clinic")
public class SimpleJdbcClinic implements Clinic, SimpleJdbcClinicMBean {
private SimpleJdbcTemplate simpleJdbcTemplate;
private SimpleJdbcInsert insertOwner;
private SimpleJdbcInsert insertPet;
private SimpleJdbcInsert insertVisit;
private final List<Vet> vets = new ArrayList<Vet>();
:
:
#Transactional(readOnly = true)
public Collection<Vet> getVets() throws DataAccessException {
synchronized (this.vets) {
if (this.vets.isEmpty()) {
refreshVetsCache();
}
return this.vets;
}
}
}
The controller mapping:
#RequestMapping("/vets")
public ModelMap vetsHandler() {
Vets vets = new Vets();
vets.getVetList().addAll(this.clinic.getVets());
return new ModelMap(vets);
}
Once the vets list is created it survives multiple requests.
Thanks
I think it avoids redundant database calls by storing all vets in the private final List<Vet> vets. Also vets variable is a property of a singleton #Service SimpleJdbcClinic

Resources