How to cache bson ObjectId - spring

I have a question regarding ObjectId caching.
I am caching mongoDB document using redis, and when I check the cached value, only the timeStamp and date of the ObjectId are cached as shown below, and they are being retrieved with values different from the ObjectId of the actual document.
"id":{
"#class":"org.bson.types.ObjectId",
"timestamp":1658025133,
"date":["java.util.Date",1658025133000]
},
Actual ObjectId: 6311ba39c31566544746d31b
ObjectId retrieved as cached result: 6311ba3911d1d82cb7892c73
How can I cache it so that it is fetched as an actual ObjectId value?

You need to add custom serializer to serialize ObjectId into String
#JsonComponent
class ObjectIdSerializer : StdSerializer<ObjectId>(ObjectId::class.java) {
override fun serialize(value: ObjectId, gen: JsonGenerator, provider: SerializerProvider) {
return gen.writeString(value.toString())
}
}
Then
objectMapper().registerModule(SimpleModule().addSerializer(ObjectIdSerializer()))

Related

RestTemplate fetch nested json from response

I need to consume a REST endpoint using spring RestTemplate. Below is the sample response from the endpoint and I need to fetch nested employee json and map it to model object. I found various complex solutions like
Deserializing response to a wrapper object and then fetching Employee object from it
Getting the response as a string and then converting it to json and deserializing
None of these solutions are simple and clean. Not sure if there is a clean and automatic way of doing it like this
ResponseEntity<Employee> response = restTemplate.exchange(URL,..?..);
Response -
{
"employee": {
"id": "123",
"first_name": "foo",
"last_name": "bar"
},
"session": {
"id": "1212121",
"createdDate": "2022-08-18T19:35:30Z"
}
}
Model object -
public class Employee {
private long emplId;
private String fName;
private String lName;
}
RestTemplate can do all of this for you, but first, your object doesn't match your response.
Ideally, you would have a Class that has an Employee and Session, like this
public class HaveSomeClassMan {
private Employee employee;
private Session session;
}
Then you can do
HaveSomeClassMan haveSomeClassMan = restTemplate.postForObject("URL", "requestObject", HaveSomeClassMan.class);
But, since you already have a JSON string, here's how you can convert it to a JSONObject, get the Employee out of it, and use the ObjectMapper to convert it to an Employee object.
JSONObject jsonObject = new JSONObject(s); // where s is your json string
String theEmployee = jsonObject.get("employee").toString(); // get the "employee" json from the JSONObject
Employee employee = new ObjectMapper().readValue(theEmployee, Employee.class); // Use the object mapper to convert it to an Employee
You do need to pay attention, though, as your model doesn't line up with your json. Your model calls the first name field, fname, yet your json has this as first_name. These properties all need to line up. If you want to keep your model as you've defined it, you need to annotate your fields with what their corresponding json field is, like this:
#JsonProperty("first_name")
private String fName;
This tells the mapper to map the JSON field first_name to the property fName. Regardless of whether you let RestTemplate do the mapping for you or you do it manually via the JSONObject approach, your model needs to match your JSON string. You can do this implicitly just by naming the fields in your model the same as they are in your JSON, or explicitly by adding the #JsonProperty to each field that doesn't implicitly map.

Spring Boot and Mongo - findById only works with ObjectID and not Id of String

I have a Spring Boot GraphQL service which reads from Mongo.
I've noticed that if my MongoDB document ID has ObjectID e.g. "_id": ObjectID("5e5605150") I'm able to get results from doing myRepository.findById(myId).
However if that id is just a string like "_id": "5e5605150" then findById returns nothing.
The repository looks like this:
#Repository
interface MyRepository : MongoRepository<Song, String>
And that Song looks like this:
#Document(collection = Song.COLLECTION)
data class Song(
#Id
val id: String,
val title: String
) {
companion object {
const val COLLECTION: String = "song"
}
}
Any ideas?
Thanks.
See the Spring Data MongoDB reference on mapping - the _id field is treated specially. If it's a String or BigInteger type, it will automatically be converted to/from an ObjectId. MongoDB supports plain strings for the special _id field though, so if you're inserting documents that don't go through Spring Data (or inserting documents as a raw JSON string for the MongoDB driver to insert) then MongoDB won't automatically convert strings to ObjectId - it will store them as strings.
So, if you insert the document {"_id": "5e5605150"} into your DB, the Spring Data MongoDB mapper won't be able to find it, because it will be searching by ObjectId only.

Sending POST request with Postman with #DBref

I want to send a POST request with Postman that creates a Purchase object and save it in the database.
My class Purchase:
#Document(collection = "purchases")
public class Purchase {
#Id
private String id;
#DBRef
private User buyer;
#DBRef
private List<File> FilesToPurchase;
private Long timestamp;
public Purchase() { }
public Purchase(User buyer, List<File> filesToPurchase) {
this.buyer = buyer;
FilesToPurchase = filesToPurchase;
}
// Getters and setters not posted here.
I want to insert in the database a new purchase done by an already existing User "buyer" who wants to purchase a list of already exsting Files "FilesToPurchase".
I have in my controller a POST function that receives a Purchase object using the annotation #RequestBody, but so far I've got NullPointerExceptions because of the empty Purchase object received.
I don't know how to handle #DBRef annotation. In Postman I try sending a JSON like this:
{
"buyer": {
"$ref":"users",
"$id" : "ObjectId('5bb5d6634e5a7b2bea75d4a2')"
},
"FilesToPurchase": [
{ "$ref":"files",
"$id" : "ObjectId('5bb5d6634e5a7b2bea75d4a5')"
}
]
}
Rename field "FilesToPurchase" and setter to "filesToPurchase" to match java conventions and try this
{ "buyer": { "id" : "5bb5d6634e5a7b2bea75d4a2" }, "filesToPurchase": [ { "id" : "5bb5d6634e5a7b2bea75d4a5" } ] }
By marking controller parameter with #RequestBody you ask Spring to deserialize input json to java object(Jackson ObjectMapper is used by default). It will not automaticly populate the whole #Dbref field, you should do it yourself by querying mongo if you want, however the only field you need in referred object to save object that reffers it is 'id'.

Spring Cache with Spring Data Rest Repository is caching results of save on exception

I have a Spring Data Rest repository that I would like to use Spring Caching with it.
The cache works as expected when there are no errors. However, when the result of the save is a validation exception, the entity is cached, even though it is not written to the database.
SDR Repository
#RepositoryRestResource(collectionResourceRel = "businessUnits", path = "businessUnits")
public interface BusinessUnitRepository extends JpaRepository<BusinessUnit, UUID> {
#Override
#Cacheable(value = "lookups", cacheManager = "vaultCache")
BusinessUnit findOne(final UUID id);
#Override
#CacheEvict(value = "lookups", cacheManager = "vaultCache", allEntries = true, beforeInvocation = true)
BusinessUnit save(BusinessUnit entity);
If I POST the message body below a new Business Unit it is saved correctly. The first GET to retrieve the record hits the database as expected and subsequent GETs come from the cache.
{
"name": "Test_Business_2",
"effectiveDate": "2019-12-16T11:11:11.111+0000",
"expirationDate": "2020-12-16T11:11:11.111+0000",
"businessUnitType": "/businessUnitTypes/38faf33c-5454-4245-bc69-2b31e510fa6b"
}
The BusinessUnit Entity has a not null on the effectiveDate field.
#NotNull
#Column(name = "effective_date")
private Timestamp effectiveDate;
When I PATCH the business unit with a null effectiveDate. The response is an error, an exception is logged and the database is not updated.
{
"effectiveDate": null
}
When I query the db, I see the record was not updated
NAME EFFECTIVE_DATE EXPIRATION_DATE
Test_Business_2,2019-12-16 06:11:11,2020-12-16 06:11:11
However, when I do a GET on the businessUnits endpoint. The database is not hit to read the record, and the null effectiveDate is returned.
{
"createDate": "2017-02-20T13:38:00.386+0000",
"lastModifiedDate": "2017-02-20T13:38:00.386+0000",
"effectiveDate": null,
"expirationDate": "2020-12-16T11:11:11.111+0000",
"name": "Test_Business_2", ...
So, it seems the data that caused the exception to be thrown, is stored in cache.
What do I need to change so that the value is not saved in cache when the database is not updated on save?
I appreciate any help or advice. Thanks.
The key you cache and the key you evict don't match. You need to define how to get the id from the BusinessUnit, for example:
#CacheEvict(key = "#entity.id"
Or provide a KeyGenerator that understands your domain model.

Id field handling in Spring Data Mongo for child objects

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.

Resources