How to save and query dynamic fields in Spring Data MongoDB? - java

I'm on Spring boot 1.4.x branch and Spring Data MongoDB.
I want to extend a Pojo from HashMap to give it the possibility to save new properties dynamically.
I know I can create a Map<String, Object> properties in the Entry class to save inside it my dynamics values but I don't want to have an inner structure. My goal is to have all fields at the root's entry class to serialize it like that:
{
"id":"12334234234",
"dynamicField1": "dynamicValue1",
"dynamicField2": "dynamicValue2"
}
So I created this Entry class:
#Document
public class Entry extends HashMap<String, Object> {
#Id
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
And the repository like this:
public interface EntryRepository extends MongoRepository<Entry, String> {
}
When I launch my app I have this error:
Error creating bean with name 'entryRepository': Invocation of init method failed; nested exception is org.springframework.data.mapping.model.MappingException: Could not lookup mapping metadata for domain class java.util.HashMap!
Any idea?

TL; DR;
Do not use Java collection/map types as a base class for your entities.
Repositories are not the right tool for your requirement.
Use DBObject with MongoTemplate if you need dynamic top-level properties.
Explanation
Spring Data Repositories are repositories in the DDD sense acting as persistence gateway for your well-defined aggregates. They inspect domain classes to derive the appropriate queries. Spring Data excludes collection and map types from entity analysis, and that's why extending your entity from a Map fails.
Repository query methods for dynamic properties are possible, but it's not the primary use case. You would have to use SpEL queries to express your query:
public interface EntryRepository extends MongoRepository<Entry, String> {
#Query("{ ?0 : ?1 }")
Entry findByDynamicField(String field, Object value);
}
This method does not give you any type safety regarding the predicate value and only an ugly alias for a proper, individual query.
Rather use DBObject with MongoTemplate and its query methods directly:
List<DBObject> result = template.find(new Query(Criteria.where("your_dynamic_field")
.is(theQueryValue)), DBObject.class);
DBObject is a Map that gives you full access to properties without enforcing a pre-defined structure. You can create, read, update and delete DBObjects objects via the Template API.
A last thing
You can declare dynamic properties on a nested level using a Map, if your aggregate root declares some static properties:
#Document
public class Data {
#Id
private String id;
private Map<String, Object> details;
}

Here we can achieve using JSONObject
The entity will be like this
#Document
public class Data {
#Id
private String id;
private JSONObject details;
//getters and setters
}
The POJO will be like this
public class DataDTO {
private String id;
private JSONObject details;
//getters and setters
}
In service
Data formData = new Data();
JSONObject details = dataDTO.getDetails();
details.put("dynamicField1", "dynamicValue1");
details.put("dynamicField2", "dynamicValue2");
formData.setDetails(details);
mongoTemplate.save(formData );
i have done as per my business,refer this code and do it yours. Is this helpful?

Related

Spring Data Elasticsearch: Convert a String to an Object (and vice versa) using ValueConverter and dot-notation

I have kind of a combination-follow up question to [1] and [2].
I have a POJO with a field I want to persist in - and read from - Elasticsearch:
#Document
public class MyPojo {
private String level3;
// getters/setters...
}
For convenience and because the property is also being persisted (flatly) into postgres, the property level3 should be a String, however it should be written into ES as a nested object (because the ES index is defined elsewhere).
The current solution is unsatisfactory:
#Document
#Entity
public class MyPojo {
#Column(name = "level3)
#Field(name = "level3", type = FieldType.Keyword)
#ValueConverter(MyConverter.class)
private String level3;
// getters/setters...
}
with the object path "level1.level2.level3" hardcoded within MyConverter, which converts from Map<String, Object> to String (read) and from String to Map<String, Object> (write). Because we potentially need to do this on multiple fields, this is not a really viable solution.
I'd rather do something like this:
#Document
#Entity
public class MyPojo {
#Column(name = "level3)
#Field(name = "level1.level2.level3", type = FieldType.Keyword)
#ValueConverter(MyConverter2.class)
private String level3;
// getters/setters...
}
which does not work (writing works fine, while reading we get the "null is not a map" error from [2]).
Is this at all possible (if I understood [2] correctly, no)? If not, is there another way to achieve what I want without hardcoding and an extra converter per field?
Can I somehow access the #Field annotation within MyConverter (e.g. the name), or can I somehow supply additional arguments to MyConverter?
[1] Spring data elasticsearch embedded field mapping
[2] Spring Elasticsearch: Adding fields to working POJO class causes IllegalArgumentException: null is not a Map

How does JPA EntityGraph allow chosing the entity graph at runtime?

I have seen various post describing that JPA EntityGraph allows to choose the graph at run time. And I am not entirely clear what this refers to.
Out of good faith and respect I would like to take this helpful article for reference: https://www.baeldung.com/jpa-entity-graph. (Most of JPA users might have gone through it already.)
The article quotes -
EntityGraph allows grouping the related persistence fields which we
want to retrieve and lets us choose the graph type at runtime.
and again solidifies above statement in conclusion section.
In this article, we've explored using the JPA Entity Graph to
dynamically fetch an Entity and its associations.
The decision is made at runtime in which we choose to load or not the
related association.
As we see in the article (5.1) - EntityGraphs are defined as below using Annotations-
5.1. Defining an Entity Graph with Annotations
#NamedEntityGraph(
name = "post-entity-graph",
attributeNodes = {
#NamedAttributeNode("subject"),
#NamedAttributeNode("user"),
#NamedAttributeNode("comments"),
}
)
#Entity
public class Post {
#OneToMany(mappedBy = "post")
private List<Comment> comments = new ArrayList<>();
//...
}
The #NameEntityGraph annotation is defined at compile time and I don't see anything runtime or dynamic here.
But in 5.2 - entity graphs are defined using api or programmatically -
5.2. Defining an Entity Graph with the JPA API
EntityGraph<Post> entityGraph = entityManager.createEntityGraph(Post.class);
entityGraph.addAttributeNodes("subject");
entityGraph.addAttributeNodes("user");
In 5.2 approach, I see nodes can be chosen dynamically using some logic. So is this approach is what is refered to "dynamically fetch" and "runtime based".
Or am i missing something and do i have more to understand.
Further the approaches given in 6. Using the Entity Graph
ex:
EntityGraph entityGraph = entityManager.getEntityGraph("post-entity-graph");
Map<String, Object> properties = new HashMap<>();
properties.put("javax.persistence.fetchgraph", entityGraph);
Post post = entityManager.find(Post.class, id, properties);
are all programmatic and so can be changed during runtime i.e they can be said as dynamic.
But one approach missed in above article, but mentioned here - https://www.baeldung.com/spring-data-jpa-named-entity-graphs, as below, does not seem to fit in to dynamic criteria.
public interface ItemRepository extends JpaRepository<Item, Long> {
#EntityGraph(value = "Item.characteristics")
Item findByName(String name);
}
So does the dynamic approach just refer to 5.2 style or it implies even 5.1 style too.
You can't use dynamic entity graphs with spring-data, because JpaRepository doesn't have methods to pass entity graphs like
Optional<T> findById(ID id, EntityGraph entityGraph);
Using custom JPA repository
You can use raw JPA for that, by creating a custom repository and using entity graphs with EntityManager.
Using spring-data-jpa-entity-graph
There is a more convenient approach by using library
spring-data-jpa-entity-graph.
It allows to use JPA repository methods like findById() or findByName() with dynamic entity graphs.
I prefer to use it with this helper class
public abstract class EntityGraphBuilder<T> {
private List<String> result = new ArrayList<>();
protected T self;
public T add(String path) {
result.add(path);
return self;
}
public DynamicEntityGraph build() {
return new DynamicEntityGraph(EntityGraphType.FETCH, result);
}
}
Each entity has its own GraphBuilder
#Entity
public class OrderEntity {
#Id
private Long id;
#Column
private name;
#ManyToOne(fetch = FetchType.LAZY)
private OrderRequestEntity orderRequest;
#ManyToOne(fetch = FetchType.LAZY)
private ProviderEntity provider;
public static GraphBuilder graph() {
return new GraphBuilder();
}
public static class GraphBuilder extends EntityGraphBuilder<GraphBuilder> {
private GraphBuilder() {
self = this;
}
public GraphBuilder orderRequest() {
return add("orderRequest");
}
public GraphBuilder provider() {
return add("provider");
}
}
}
Repository uses EntityGraphJpaRepository from spring-data-jpa-entity-graph library
#Repository
public interface OrdersRepository extends EntityGraphJpaRepository<OrderEntity, Long> {
OrderEntity findByName(String name, EntityGraph entityGraph);
}
You can use derived query methods like findByName() with dynamic entity graphs too.
Example of using findById() method, the same approach can be applied to findByName()
OrdersRepository ordersRepository;
Long orderId = 1L;
OrderEntity order = ordersRepository.findById(
orderId,
OrderEntity.graph().orderRequest().provider().build()
).orElseThrow(
() -> new ServiceException("Can't find orderId=" + orderId)
);
In the Baeldung article, Section 5 is only about various ways to define a graph with not much emphasis on the dynamic/non-dynamic nature of the definition itself.
In Section 5.1 the definition of the graph is static but this section is only about demonstrating how to define a graph which then will be used in typical cases when building the graph dynamically is not really necessary. This section shows an alternative to the older way of building HQL / JPA-QL queries with JOIN FETCH sections.
#NamedEntityGraph(
name = "post-entity-graph",
attributeNodes = {
#NamedAttributeNode("subject"),
#NamedAttributeNode("user"),
#NamedAttributeNode("comments"),
}
)
#Entity
public class Post {
#OneToMany(mappedBy = "post")
private List<Comment> comments = new ArrayList<>();
//...
}
Then, Section 6 tells you how to use the entity graphs defined earlier in various ways.
// Getting the "statically" defined graph (from annotation)
EntityGraph entityGraph = entityManager.getEntityGraph("post-entity-graph");
// Then using the graph
Map<String, Object> properties = new HashMap<>();
properties.put("javax.persistence.fetchgraph", entityGraph);
Post post = entityManager.find(Post.class, id, properties);
Naturally, you can swap this first line to the fully dynamically built graph demonstrated in Section 5.2:
// Building the graph dynamically
EntityGraph<Post> entityGraph = entityManager.createEntityGraph(Post.class);
entityGraph.addAttributeNodes("subject");
entityGraph.addAttributeNodes("user");
// Then using the graph
Map<String, Object> properties = new HashMap<>();
properties.put("javax.persistence.fetchgraph", entityGraph);
Post post = entityManager.find(Post.class, id, properties);
In both cases you supply an EntityGraph object to the query.

How can I use QueryDSL on multiple mongo repositories?

I am building a profile service with the typical REST endpoints for creating, reading, updating and deleting profiles. For this I am using the Spring Framework together with a MongoDB. On top I would like to use QueryDSL to create some custom queries.
A full minimal working example of the current implementation can be found here: https://github.com/mirrom/profile-modules
I would like to have sub profile models that extend the base profile model, and sub sub models that extend the sub models. By this I have hierarchical profiles that inherit the fields of its parent profile. The idea is to store all profiles in the same collection and distinguish them via the automatically created _class field.
A simple example (with Lombok annotations):
#Data
#Document(collection = "profiles")
#Entity
public class Profile {
#Id
private ObjectId id;
#Indexed
private String title;
#Indexed
private String description;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
}
#Data
#Entity
#EqualsAndHashCode(callSuper = true)
public class Sub1Profile extends Profile {
private String sub1String;
private int sub1Integer;
}
While (all) profiles can get accessed via the endpoint /api/v1/profiles, the sub1Profiles can be accessed via /api/v1/profiles/sub-1-profiles. Currently the sub1Profiles endpoint delivers all profiles, but it should just deliver the sub1Profiles and its children. For this I would like to use QueryDSL, but I can't add QuerydslPredicateExecutor<Profile> and QuerydslBinderCustomizer<QProfile> to more than one repository interface. This is how my profile repository looks like:
#Repository
public interface ProfileRepository extends MongoRepository<Profile, ObjectId>, QuerydslPredicateExecutor<Profile>,
QuerydslBinderCustomizer<QProfile> {
#Override
default void customize(QuerydslBindings bindings, QProfile root) {
bindings.bind(String.class)
.first((SingleValueBinding<StringPath, String>) StringExpression::containsIgnoreCase);
}
}
If I now try to do the same with Sub1ProfileRepository:
#Repository
public interface Sub1ProfileRepository
extends MongoRepository<Sub1Profile, ObjectId>, QuerydslPredicateExecutor<Sub1Profile>,
QuerydslBinderCustomizer<QSub1Profile> {
default void customize(QuerydslBindings bindings, QProfile root) {
bindings.bind(String.class)
.first((SingleValueBinding<StringPath, String>) StringExpression::containsIgnoreCase);
}
}
I get this error message:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sub1ProfileRepository' defined in com.example.profile.repository.sub1profile.Sub1ProfileRepository defined in #EnableMongoRepositories declared on MongoRepositoriesRegistrar.EnableMongoRepositoriesConfiguration: Invocation of init method failed; nested exception is org.springframework.data.mapping.PropertyReferenceException: No property customize found for type Sub1Profile!
What am I missing?
In Sub1ProfileRepository's customize method, you have used QProfile as method argument. Can you use QSub1Profile instead and check if it's working?

Converting params to a POJO to query H2 database

Currently implementing a url based query spring controller and take an example query
search/?driverId=1&driverId=2.
The controller looks like the following:
public List<DriverDTO> searchDrivers(#RequestParam Map<String,
String> params) {}
My main concern, is how to map the Map params to a POJO like below, so that i can use that map to query the H2 database. Or is it possible to query directly to the database?
public class queryDO {
private Long id;
private ManufacturerType manufacturerType;
private int rating;
}
This is my first time doing this type of query api so any examples (can't find any at the moment on github).
Th following should work just fine if your QueryDO class has getters and setters:
public class QueryDO {
private Long id;
private ManufacturerType manufacturerType;
private int rating;
// getters / setters
}
#RestController
public class SearchController {
#GetMapping("/search")
public List<DriverDTO> searchDrivers(QueryDO query) {
...
}
}
If parameter name from query strings matches the property of the QueryDO class, Spring will map automatically it into the corresponding property of the query object.
For example query:
search/?id=1&rating=35

Spring Data Elasticsearch: Multiple Index with same Document

I'm using spring-data-elasticsearch and for the beginning everything works fine.
#Document( type = "products", indexName = "empty" )
public class Product
{
...
}
public interface ProductRepository extends ElasticsearchRepository<Product, String>
{
...
}
In my model i can search for products.
#Autowired
private ProductRepository repository;
...
repository.findByIdentifier( "xxx" ).getCategory() );
So, my problem is - I've the same Elasticsearch type in different indices and I want to use the same document for all queries. I can handle more connections via a pool - but I don't have any idea how I can implement this.
I would like to have, something like that:
ProductRepository customerRepo = ElasticsearchPool.getRepoByCustomer("abc", ProductRepository.class);
repository.findByIdentifier( "xxx" ).getCategory();
Is it possible to create a repository at runtime, with an different index ?
Thanks a lot
Marcel
Yes. It's possible with Spring. But you should use ElasticsearchTemplate instead of Repository.
For example. I have two products. They are stored in different indices.
#Document(indexName = "product-a", type = "product")
public class ProductA {
#Id
private String id;
private String name;
private int value;
//Getters and setters
}
#Document(indexName = "product-b", type = "product")
public class ProductB {
#Id
private String id;
private String name;
//Getters and setters
}
Suppose if they have the same type, so they have the same fields. But it's not necessary. Two products can have totally different fields.
I have two repositories:
public interface ProductARepository extends ElasticsearchRepository<ProductA, String> {
}
public interface ProductBRepository
extends ElasticsearchRepository<ProductB, String> {
}
It's not necessary too. Only for testing. The fact that ProductA is stored in "product-a" index and ProductB is stored in "product-b" index.
How to query two(ten, dozen) indices with the same type?
Just build custom repository like this
#Repository
public class CustomProductRepositoryImpl {
#Autowired
private ElasticsearchTemplate elasticsearchTemplate;
public List<ProductA> findProductByName(String name) {
MatchQueryBuilder queryBuilder = QueryBuilders.matchPhrasePrefixQuery("name", name);
//You can query as many indices as you want
IndicesQueryBuilder builder = QueryBuilders.indicesQuery(queryBuilder, "product-a", "product-b");
SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(builder).build();
return elasticsearchTemplate.query(searchQuery, response -> {
SearchHits hits = response.getHits();
List<ProductA> result = new ArrayList<>();
Arrays.stream(hits.getHits()).forEach(h -> {
Map<String, Object> source = h.getSource();
//get only id just for test
ProductA productA = new ProductA()
.setId(String.valueOf(source.getOrDefault("id", null)));
result.add(productA);
});
return result;
});
}
}
You can search as many indices as you want and you can transparently inject this behavior into ProductARepository adding custom behavior to single repositories
Second solution is to use indices aliases, but you had to create custom model or custom repository too.
We can use the withIndices method to switch the index if needed:
NativeSearchQueryBuilder nativeSearchQueryBuilder = nativeSearchQueryBuilderConfig.getNativeSearchQueryBuilder();
// Assign the index explicitly.
nativeSearchQueryBuilder.withIndices("product-a");
// Then add query as usual.
nativeSearchQueryBuilder.withQuery(allQueries)
The #Document annotation in entity will only clarify the mapping, to query against a specific index, we still need to use above method.
#Document(indexName="product-a", type="_doc")

Categories

Resources