Id field handling in Spring Data Mongo for child objects - spring-boot

I have been working in Spring Boot with the Spring Data MongoDB project and I am seeing behavior I am not clear on. I understand that the id field will go to _id in the Mongo repository per http://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#mapping.conventions.id-field. My problem is that it also seems to be happening for child entities which does not seem correct.
For example I have these classes (leaving out setters and getters for brevity) :
public class MessageBuild {
#Id
private String id;
private String name;
private TopLevelMessage.MessageType messageType;
private TopLevelMessage message;
}
public interface TopLevelMessage {
public enum MessageType {
MapData
}
}
public class MapData implements TopLevelMessage {
private String layerType;
private Vector<Intersection> intersections;
private Vector<RoadSegment> roadSegments;
}
public class RoadSegment {
private int id;
private String name;
private Double laneWidth;
}
and I create an object graph using this I use the appropriate MongoRepository class to save I end up with an example document like this (with _class left out):
{
"_id" : ObjectId("57c0c05568a6c4941830a626"),
"_class" : "com.etranssystems.coreobjects.persistable.MessageBuild",
"name" : "TestMessage",
"messageType" : "MapData",
"message" : {
"layerType" : "IntersectionData",
"roadSegments" : [
{
"_id" : 2001,
"name" : "Road Segment 1",
"laneWidth" : 3.3
}
]
}
}
In this case a child object with a field named id has its mapping converted to _id in the MongoDB repository. Not the end of the world although not expected. The biggest problem is now that this is exposed by REST MVC the _id fields are not returned from a query. I have tried to set the exposeIdsFor in my RepositoryRestConfigurerAdapter for this class and it exposes the id for the top level document but not the child ones.
So circling around the 2 questions/issues I have are:
Why are child object fields mapped to _id? My understanding is that this should only happen on the top level since things underneath are not really documents in their own right.
Shouldn't the configuration to expose id fields work for child objects in a document if it is mapping the field names?

Am I wrong to think that RoadSegment does not contain a getId() ? From Spring's documentation:
A property or field without an annotation but named id will be mapped
to the _id field.
I believe Spring Data does this even to nested classes, when it finds an id field. You may either add a getId(), so that the field is named id or annotate it with #Field:
public class RoadSegment {
#Field("id")
private int id;
private String name;
private Double laneWidth;
}
I agree this automatic conversion of id/_id should only be done at the top level in my opinion.
However, the way Spring Data Mongo conversion is coded, all java ojects go through the exact same code to be converted into json (both top and nested objects):
public class MappingMongoConverter {
...
protected void writeInternal(Object obj, final DBObject dbo, MongoPersistentEntity<?> entity) {
...
if (!dbo.containsField("_id") && null != idProperty) {
try {
Object id = accessor.getProperty(idProperty);
dbo.put("_id", idMapper.convertId(id));
} catch (ConversionException ignored) {}
}
...
if (!conversions.isSimpleType(propertyObj.getClass())) {
// The following line recursively calls writeInternal with the nested object
writePropertyInternal(propertyObj, dbo, prop);
} else {
writeSimpleInternal(propertyObj, dbo, prop);
}
}
writeInternal is called on the top level object, and then recalled recursively for each subobjects (aka SimpleTypes). So they both go through the same logic of adding _id.
Perhaps this is how we should read Spring's documentation:
Mongo's restrictions on Mongo Documents:
MongoDB requires that you have an _id field for all documents. If you
don’t provide one the driver will assign a ObjectId with a generated
value.
Spring Data's restrictions on java classes:
If no field or property specified above is present in the Java class
then an implicit _id file will be generated by the driver but not
mapped to a property or field of the Java class.

Related

Spring data mongo - unique random generated field

I'm using spring data mongo. I have a collection within a document that when I add an item to it I would like to assign a new automatically generated unique identifier to it e.g. (someGeneratedId)
#Document(collection = "questionnaire")
public class Questionnaire {
#Id
private String id;
#Field("answers")
private List<Answer> answers;
}
public class Answer {
private String someGeneratedId;
private String text;
}
I am aware I could use UUID.randomUUID() (wrapped in some kind of service) and set the value, I was just wondering if there was anything out of the box that can handle this? From here #Id seems to be specific to _id field in mongo:
The #Id annotation tells the mapper which property you want to use for
the MongoDB _id property
TIA
No there is no out of the box solution for generating ids for properties on embedded documents.
If you want to keep this away from your business-logic you could implement a BeforeConvertCallback which generates the id's for your embedded objects.
#Component
class BeforeConvertQuestionnaireCallback implements BeforeConvertCallback<Questionnaire> {
#Override
public Questionnaire onBeforeConvert(#NonNull Questionnaire entity, #NonNull String collection) {
for (var answer : entity.getAnswers()) {
if (answer.getId() == null) {
answer.setId(new ObjectId().toString());
}
}
return entity;
}
}
You could also implement this in a more generic manner:
Create a new annotation: #AutogeneratedId.
Then listen to all BeforeConvertCallback's of all entities and iterate through the properties with reflection. Each property annotated with the new annotation gets a unique id if null.

#Field(type=FieldType.keyword) being ignored on certain properties

I'm having trouble with the way SD Elasticsearch is creating some of my indices on application startup. I've got some String fields that I want to be of type "keyword" but they are always being created as type "text". This is using Elasticsearch 5.5.1, Spring 5.0.0, Spring Data Kay-RELEASE.
As an example I've got something like follows:
// DepartmentSearchResult.java
#Document(indexName = "hr_index", type = "department", createIndex = false)
public class DepartmentSearchResult implements Serializable {
#Id private String id;
private String foo;
// other fields, getters, setters etc. omitted
}
// DepartmentSearchingRepository.java
public interface DepartmentSearchingRepository extends ElasticsearchRepository<DepartmentSearchResult, String> {}
// ApplicationStartupListener.java
#Component
public class ApplicationStartupListener implements ApplicationListener<ContextRefreshedEvent> {
#Autowired ElasticsearchTempalte elasticSearchTemplate;
#Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (!elasticSearchTemplate.indexExists(DepartmentSearchResult.class)) {
elasticSearchTemplate.createIndex(DepartmentSearchResult.class);
elasticSearchTemplate.putMapping(DepartmentSearchResult.class);
}
}
}
(I've set createIndex to false and have the ApplicationStartupListener instead due to an issue I asked about at How to correctly address "can't add a _parent field that points to an already existing type, that isn't already a parent" on app startup. Including it here in case this is somehow related.)
Anyway, I want DepartmentSearchResult.id to be keyword, not text. So, I've changed from:
#Id private String id;
to:
#Field(type=FieldType.keyword) #Id private String id;
I then drop the index and restart my app. The index and the mapping are created, but "id" is always created as text, not keyword. Other fields are working properly; if I change foo to #Field(type=FieldType.keyword) private String foo; that shows up as keyword in the mapping, it's just this id field that I can't get working.
My current workaround is to just create the mapping manually like:
curl -s -X PUT http://localhost:9200/hr_index/_mapping/department -d '{
"properties": {
"id": {
"type": "keyword"
}
}
}'
and that works, but I'd much prefer for Spring to just create the mapping on the fly so I don't need to maintain a separate mapping installation script. Any places I should be looking that might indicate why this field keeps getting created as "text"?

Spring Boot Controller endpoint and ModelAttribute deep access

I would like to know how to access a deep collection class attribute in a GET request. My endpoint maps my query strings through #ModelAttribute annotation:
Given that:
public class MyEntity
{
Set<Item> items;
Integer status;
// getters setters
}
public class Item
{
String name;
// getters setters
}
And my GET request: localhost/entities/?status=0&items[0].name=Garry
Produces bellow behavior?
#RequestMapping(path = "/entities", method = RequestMethod.GET)
public List<MyEntity> findBy(#ModelAttribute MyEntity entity) {
// entity.getItems() is empty and an error is thrown: "Property referenced in indexed property path 'items[0]' is neither an array nor a List nor a Map."
}
Should my "items" be an array, List or Map? If so, there´s alternatives to keep using as Set?
Looks like there is some problem with the Set<Item>.
If you want to use Set for the items collection you have to initialize it and add some items:
e.g. like this:
public class MyEntity {
private Integer status;
private Set<Item> items;
public MyEntity() {
this.status = 0;
this.items = new HashSet<>();
this.items.add(new Item());
this.items.add(new Item());
}
//getters setters
}
but then you will be able to set only the values of this 2 items:
This will work: http://localhost:8081/map?status=1&items[0].name=asd&items[1].name=aaa
This will not work: http://localhost:8081/map?status=1&items[0].name=asd&items[1].name=aaa&items[2].name=aaa
it will say: Invalid property 'items[2]' of bean class MyEntity.
However if you switch to List:
public class MyEntity {
private Integer status;
private List<Item> items;
}
both urls map without the need to initialize anything and for various number of items.
note that I didn't use #ModelAttribute, just set the class as paramter
#GetMapping("map")//GetMapping is just a shortcut for RequestMapping
public MyEntity map(MyEntity myEntity) {
return myEntity;
}
Offtopic
Mapping a complex object in Get request sounds like a code smell to me.
Usually Get methods are used to get/read data and the url parameters are used to specify the values that should be used to filter the data that has to be read.
if you want to insert or update some data use POST, PATCH or PUT and put the complex object that you want to insert/update in the request body as JSON(you can map that in the Spring Controller with #RequestBody).

Spring data mongodb repository. How can I search by a list of IDs?

I have the following class!
public class Task{
ObjectId id;
String title;
String description;
/* Getters and Setters removed for brevity */
}
and I have the following mongoRepository class, very simple :
public interface TaskRepository extends MongoRepository<Task, String> {
}
As you can see, I have not yet tried to extend this class - What would I want to do here if I want to have a find method, where I could just hand it a list of Ids, and get my list of corresponding tasks back?
The CrudRepository which MongoRepository extends has a findAll method, which takes an Itereable<ID>
I think that is exactly what you are looking for.
Note that it is renamed to findAllById in the latest Milestone releases.
You can create a custom query method that searches for an array of _id values:
#Query(value = "{ '_id' : {'$in' : ?0 } }", fields = "{ 'description': 0 }")
Iterable<Task> findAllThin(Iterable<String> ids);
(in this case it returns the fields id and title only)
#Neil Lunn brought me to the answer.

Spring-Mongo : mapping mongo document field/s to BasicDBObject/Map of BasicDBObject of an Entity

I have an entity ProjectCycle mapped to mongo DB collection ProjectCycle. I am trying to retrieve 2 fields, _id and Status. I am able to retrieve both like the following
#Document(collection="ProjectCycle")
public class ProjectCycle {
#Id
private String id;
#Field("Status")
private String status;
//getters and setters
}
Application.java
Query query = new Query();
query.fields().include("Status");
Criteria criteria = new Criteria();
criteria.and("_id").is("1000");
query.addCriteria(criteria);
Iterable<ProjectCycle> objectList = mongoOperations.find(query, ProjectCycle.class);
for(ProjectCycle obj : objectList) {
System.out.println("_id "+obj.getId());
System.out.println("status "+obj.getStatus());
}
Output
_id 1000
status Approved
But, the problem is when i use an Entity with field private DBObject basicDbObject; instead of private String status; i am getting value as null instead of Approved
I have tried like the following
public class ProjectCycle {
#Id
private String id;
private DBObject basicDbObject;
//getter & setter
}
What I am trying to achieve is that, the collection 'ProjectCycle' is very large and creating a POJO corresponding to it is quiet difficult. Also I am only reading data from mongoDB. So creating the entire POJO is time wasting and tedious.
How I can achieve mapping between any field/fields from mongo Collection to entity?.
Will it be possible to create a Map<String, BasicDBObject> objectMap; to fields returned from query? I am using Spring-data-mongodb for the same.
Version details
Spring 4.0.7.RELEASE
spring-data-mongodb 1.7.2.RELEASE
Try mapping your query like below.
Iterable<BasicDBObject> objectList = mongoOperations.find(query, BasicDBObject.class, collectionname);
for(BasicDBObject obj : objectList) {
System.out.println("_id "+obj.get("id"));
System.out.println("status "+obj.get("status"));
}

Resources