God afternoon.
I am developing an app which show the places avalaible. In the case of Backend, researched about the SSE (Server-Sent-Event) and TailableCursors with MongoDB. I have done in Spring:
#Repository
public interface Repositorio extends ReactiveMongoRepository<Prueba, String> {
#Tailable
Flux<Prueba> findWithTailableCursorBy();
}
And the controller:
#RestController
#CrossOrigin
#RequestMapping
public class Controlador {
#Autowired
Repositorio repo;
#GetMapping(value = "/prueba", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Prueba> getTodos() {
return repo.findWithTailableCursorBy();
}
#GetMapping("/prueba2")
public Flux<Prueba> prueba() {
return repo.findAll();
}
#PostMapping("/prueba")
public Mono<Prueba> insert(#RequestBody Prueba prueba) {
return repo.save(prueba);
}
}
The items don't change when updated. I thought that with Tailable Cursors I would solve it. However, I see that no...
Is there any way to show the rafters in real time with Spring? So I'm using EventSource on the client-side.
PS: The class model (a capped collection) is:
#Document(collection = "pruebas")
// Lombok's annotations
#Data #NoArgsConstructor #AllArgsConstructor
#EqualsAndHashCode #ToString
public class Prueba {
#Id private String id;
private String nombre;
private boolean activo = false;
}
Related
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);
}
}
I use snake_case DB columns and camelCase DTO.
And our team want to use snake_case when we code React component.
Because of it, I added #JsonNaming on DTO. But it works when I send Json data, as you know.
Is there any annotation or setting similar to #JsonNaming?
Here is my postman data and sample codes.
Debug data: sampleName=name, sampleDesc=null.
// Controller
#RestController
#RequestMapping("/sample")
public class SampleController {
#Autowired
private SampleService sampleService;
#GetMapping
public Result getSampleList(SampleDTO param) throws Exception {
return sampleService.getFolderList(param);
}
#PostMapping
public Result insertSample(#RequestBody SampleDTO param) throws Exception {
// this method works well with #JsonNaming
return sampleService.insertFolder(param);
}
}
// DTO
#Setter
#Getter
#NoArgsConstructor
#JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
#Alias("SampleDTO")
public class SampleDTO {
#NotNull
private Long sampleNo;
#NotBlank
private String sampleName;
private String sampleDesc;
#Builder
public SampleDTO(Long sampleNo, String sampleName, String sampleDesc) {
this.sampleNo = sampleNo;
this.sampleName = sampleName;
this.sampleDesc = sampleDesc;
}
}
I had the same problem and didn't find an annotation for this but maybe you can use #ConstructorProperties like this in your DTO's constructor:
#ConstructorProperties({"sample_no","sample_name","sample_desc"})
public SampleDTO(Long sampleNo, String sampleName, String sampleDesc) {
this.sampleNo = sampleNo;
this.sampleName = sampleName;
this.sampleDesc = sampleDesc;
}
I have a simple document:
#Document
#NoArgsConstructor
#AllArgsConstructor
#Builder
#ToString
public class ProductUnit {
#Id
String id;
private String name;
private Integer price;
private LocalDateTime localDateTime;
}
Simple MongoRepository :
public interface productRepo extends MongoRepository<ProductUnit,String> {
ProductUnit deleteByName(String name);
List<ProductUnit> findByPrice(Integer price);
}
and Service :
#Service
public class productServiseImpl implements productServise {
#Autowired
productRepo repository;
#Override
public ProductUnit saveOrUpdate(ProductUnit productUnit) {
System.out.println("inside save or update");
return repository.save(productUnit);
}
#Override
public List<ProductUnit> findAll() {
return repository.findAll();
}
#Override
public ProductUnit deleteUnitByPrice(String name) {
return repository.deleteByName(name);
}
#Override
public List<ProductUnit> findByPrice(Integer price) {
return repository.findByPrice(price);
}
}
Now , inside RestController , I pass id through a post request and use a random class to generate a random value of the price and name .At this stage everything is fine, i.e. all values were initialized correctly, but when it comes to service.saveOrUpdate(forSave) It stores the value incorrectly, i.e. the request returns an empty json and the findAll method returns a list of empty json.Can you tell me what the error is? thanks
#RestController
public class productUnitRestController {
#Autowired
productServise service;
#Autowired
Supplier<MetaInfGenerator> generatorSupplier;
#GetMapping(path = "/all")
public List<ProductUnit> getAllProoduct(){
return service.findAll();
}
#PostMapping(path = "/products")
public ProductUnit createProoduct(#RequestParam("id") Optional<String> newId){
System.out.println("***** iside PostMapping ******");
MetaInfGenerator generator = generatorSupplier.get();
System.out.println("***** supplier PostMapping ******");
ProductUnit forSave = ProductUnit.builder()
.id(newId.get())
.name(generator.getRandomString())
.price(generator.getRandomInteger())
.localDateTime(LocalDateTime.now()).build();
System.out.println(forSave);
return service.saveOrUpdate(forSave);
}
}
Hi i would like know how to have in my controller without any blocking code transformation from two arguments to Mono of DTO class.
Suppose I have controller like below:
#RestController
#RequiredArgsConstructor
class GithubRepositoryEndpoint {
private final GithubService githubService;
#GetMapping("/repositories/{owner}/{repositoryName}")
Mono<RepoDetailsResponseDTO> getRepositoryDetails(#PathVariable("owner") String owner,
#PathVariable("repositoryName") String repositoryName) {
return githubService.getRepositoryDetails(Mono.just(new RepoDetailsRequestDTO(owner, repositoryName)));
}
}
I think this line is blocking:
Mono.just(new RepoDetailsRequestDTO(owner, repositoryName)
and here is DTO class:
#Data
#Builder
#AllArgsConstructor
public class RepoDetailsRequestDTO {
private String owner;
private String repositoryName;
}
My service is not blocking:
#Slf4j
#RequiredArgsConstructor
public class GithubService {
private final GithubClient githubClient;
private final RequestValidator requestValidator;
private final DomainMapper domainMapper;
public Mono<RepoDetailsResponseDTO> getRepositoryDetails(Mono<RepoDetailsRequestDTO> request) {
return request.map(requestValidator::validate)
.map(domainMapper::mapFromDto)
.flatMap(ownerAndRepoName -> githubClient.fetchRepositoryDetails(ownerAndRepoName._1, ownerAndRepoName._2))
.onErrorResume(exc -> Mono.error(new FetchRepoDetailsException(exc.getMessage(), exc)));
}
}
the line
Mono.just(new RepoDetailsRequestDTO(owner, repositoryName))
is not blocking. It is just a constructor invocation. It does not use any blocking APIs (e.g. io/file/network). Both parameters owner and repositoryName can be safely accessed as they already contain the parsed path variables.
I can't get Spring Data Rest with class inheritance working.
I'd like to have a single JSON Endpoint which handles all my concrete classes.
Repo:
public interface AbstractFooRepo extends KeyValueRepository<AbstractFoo, String> {}
Abstract class:
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = MyFoo.class, name = "MY_FOO")
})
public abstract class AbstractFoo {
#Id public String id;
public String type;
}
Concrete class:
public class MyFoo extends AbstractFoo { }
Now when calling POST /abstractFoos with {"type":"MY_FOO"}, it tells me: java.lang.IllegalArgumentException: PersistentEntity must not be null!.
This seems to happen, because Spring doesn't know about MyFoo.
Is there some way to tell Spring Data REST about MyFoo without creating a Repository and a REST Endpoint for it?
(I'm using Spring Boot 1.5.1 and Spring Data REST 2.6.0)
EDIT:
Application.java:
#SpringBootApplication
#EnableMapRepositories
public class Application {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
I'm using Spring Boot 1.5.1 and Spring Data Release Ingalls.
KeyValueRepository doesn't work with inheritance. It uses the class name of every saved object to find the corresponding key-value-store. E.g. save(new Foo()) will place the saved object within the Foo collection. And abstractFoosRepo.findAll() will look within the AbstractFoo collection and won't find any Foo object.
Here's the working code using MongoRepository:
Application.java
Default Spring Boot Application Starter.
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
AbstractFoo.java
I've tested include = JsonTypeInfo.As.EXISTING_PROPERTY and include = JsonTypeInfo.As.PROPERTY. Both seem to work fine!
It's even possible to register the Jackson SubTypes with a custom JacksonModule.
IMPORTANT: #RestResource(path="abstractFoos") is highly recommended. Else the _links.self links will point to /foos and /bars instead of /abstractFoos.
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = Foo.class, name = "MY_FOO"),
#JsonSubTypes.Type(value = Bar.class, name = "MY_Bar")
})
#Document(collection="foo_collection")
#RestResource(path="abstractFoos")
public abstract class AbstractFoo {
#Id public String id;
public abstract String getType();
}
AbstractFooRepo.java
Nothing special here
public interface AbstractFooRepo extends MongoRepository<AbstractFoo, String> { }
Foo.java & Bar.java
#Persistent
public class Foo extends AbstractFoo {
#Override
public String getType() {
return "MY_FOO";
}
}
#Persistent
public class Bar extends AbstractFoo {
#Override
public String getType() {
return "MY_BAR";
}
}
FooRelProvider.java
Without this part, the output of the objects would be separated in two arrays under _embedded.foos and _embedded.bars.
The supports method ensures that for all classes which extend AbstractFoo, the objects will be placed within _embedded.abstractFoos.
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class FooRelProvider extends EvoInflectorRelProvider {
#Override
public String getCollectionResourceRelFor(final Class<?> type) {
return super.getCollectionResourceRelFor(AbstractFoo.class);
}
#Override
public String getItemResourceRelFor(final Class<?> type) {
return super.getItemResourceRelFor(AbstractFoo.class);
}
#Override
public boolean supports(final Class<?> delimiter) {
return AbstractFoo.class.isAssignableFrom(delimiter);
}
}
EDIT
Added #Persistent to Foo.java and Bar.java. (Adding it to AbstractFoo.java doesn't work). Without this annotation I got NullPointerExceptions when trying to use JSR 303 Validation Annotations within inherited classes.
Example code to reproduce the error:
public class A {
#Id public String id;
#Valid public B b;
// #JsonTypeInfo + #JsonSubTypes
public static abstract class B {
#NotNull public String s;
}
// #Persistent <- Needed!
public static class B1 extends B { }
}
Please see the discussion in this resolved jira task for details of what is currently supported in spring-data-rest regarding JsonTypeInfo. And this jira task on what is still missing.
To summarize - only #JsonTypeInfo with include=JsonTypeInfo.As.EXISTING_PROPERTY is working for serialization and deserialization currently.
Also, you need spring-data-rest 2.5.3 (Hopper SR3) or later to get this limited support.
Please see my sample application - https://github.com/mduesterhoeft/spring-data-rest-entity-inheritance/tree/fixed-hopper-sr3-snapshot
With include=JsonTypeInfo.As.EXISTING_PROPERTY the type information is extracted from a regular property. An example helps getting the point of this way of adding type information:
The abstract class:
#Entity #Inheritance(strategy= SINGLE_TABLE)
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME,
include=JsonTypeInfo.As.EXISTING_PROPERTY,
property="type")
#JsonSubTypes({
#Type(name="DECIMAL", value=DecimalValue.class),
#Type(name="STRING", value=StringValue.class)})
public abstract class Value {
#Id #GeneratedValue(strategy = IDENTITY)
#Getter
private Long id;
public abstract String getType();
}
And the subclass:
#Entity #DiscriminatorValue("D")
#Getter #Setter
public class DecimalValue extends Value {
#Column(name = "DECIMAL_VALUE")
private BigDecimal value;
public String getType() {
return "DECIMAL";
}
}