ClassCastException using Morphia - mongodb

I'm trying to get a collection of MongoDB and convert the records into javabeans using Morphia, but when I try to get the collection of objects (see below in App code), a cast error appears:
Exception in thread "main" java.lang.ClassCastException: com.mongodb.BasicDBObject cannot be cast to com.homework.Score
Document
{
"_id" : 19,
"name" : "Student 01",
"scores" : [
{
"type" : "exam",
"score" : 44.51211101958831
},
{
"type" : "quiz",
"score" : 0.6578497966368002
},
{
"type" : "homework",
"score" : 93.36341655949683
},
{
"type" : "homework",
"score" : 49.43132782777443
}
]
}
Student
#Entity("students")
public class Student {
#Id
private int id;
#Property("name")
private String name;
#Property("scores")
private List<Score> scores;
gets and sets
}
Score
#Embedded
public class Score {
#Property("type")
private String type;
#Property("score")
private double score;
gets and sets
}
App
private static MongoClient client = new MongoClient();
private static final Morphia morphia = new Morphia();
private static Datastore datastore;
private static Query<Student> query;
public static void main(String[] args) {
morphia.mapPackage("com.homework");
datastore = morphia.createDatastore(client, "school");
datastore.ensureIndexes();
query = datastore.createQuery(Student.class);
List<Student> students = query.asList();
List<Score> scoresCurr;
Score score1;
Score score2;
int idx;
for (Student s : students) {
scoresCurr = s.getScores();
score1 = scoresCurr.get(2); <<<< exception occurs here
score2 = scoresCurr.get(3);
idx = score1.getScore() < score2.getScore() ? 2 : 3;
scoresCurr.remove(idx);
s.setScores(scoresCurr);
datastore.save(s);
}
client.close();
}

This is the similar behavior other people have also experienced.
Can't find a codec for class , Morphia , Spring.
You can file a bug report here if you feel this is not the right behavior.
https://github.com/mongodb/morphia/issues
Okay now coming to the solution. You can fix it two ways.
Solution 1
You can remove the #Embedded annotation from score pojo and
Replace
#Property("scores")
private List<Score> scores;
with
#Embedded("scores")
private List<Score> scores;
Solution 2
Remove the #property annotation for scores field in Student pojo.

Related

Spring data mongo: Projection not working when path includes a key of a HashMap

Problem
When I try to project a value which is inside a java.util.Map then I get below exception. But when I run the generated shell query in roboMongo then It works. I would be very grateful if somebody could point out the problem.
org.springframework.data.mapping.context.InvalidPersistentPropertyPath: No property Germany found on com.nntn.corona.snapshot.repo.model.StatWithDelta!
Query Code In Java
spring boot parent: 2.0.5.RELEASE
Criteria matchCriteria = Criteria.where("timestamp").gte(startDate);
MatchOperation match = Aggregation.match(matchCriteria);
SortOperation sort = sort(new Sort(Sort.Direction.ASC, "timestamp"));
// #formatter:off
ProjectionOperation projection = project()
.andExpression("timestamp").as("timestamp")
.andExpression("countries.germany.total").as("total")
.andExpression("countries.germany.today").as("today");
// #formatter:on
Aggregation aggregation = newAggregation(match, sort,projection);
AggregationResults<Document> result = mongoTemplate.aggregate(aggregation, SnapshotEntry.class,
Document.class);
return result.getMappedResults();
Data Model
Java representation
#Document(collection = "snpashots")
public class SnapshotEntry {
#Id
private String id;
#Type(type = "org.jadira.usertype.dateandtime.joda.PersistentDateTime")
#Temporal(TemporalType.TIMESTAMP)
private DateTime timestamp;
private Map<String, StatWithDelta> countries;
private StatEntity total;
private StatEntity today;
private String source;
private String previousRecordId;
#Data
#NoArgsConstructor
#AllArgsConstructor
public class StatWithDelta {
private StatEntity total;
private StatEntity today;
}
}
Json representation
{
"_id" : "21-03-2020",
"timestamp" : ISODate("2020-03-21T09:26:00.965Z"),
"countries" : {
"germany" : {
"total" : {
"born" : NumberLong(81008),
"dead" : NumberLong(3255),
"sick" : NumberLong(30000)
},
"today" : {
"born" : NumberLong(50),
"dead" : NumberLong(10),
"sick" : NumberLong(12)
}
}
},
"_class" : "com.nntn.snapshot.repo.model.SnapshotEntry"
}
The problem is in TypedAggregation. This is a special aggregation where Spring holds information of the input aggregation type.
To avoid it, use raw aggregation (as if you run in MongoDB shell) this way:
AggregationResults<SnapshotEntry> result = mongoTemplate.aggregate(aggregation,
mongoTemplate.getCollectionName(SnapshotEntry.class),
SnapshotEntry.class);

Spring Boot: Get an object from MongoDB document?

I am using Spring Boot 2.1.3.RELEASE and MongoDB. And I am looking for a way to retrieve a part of a document.
Repository:
#Repository
public interface CompanyRepository extends MongoRepository<Company, String> {
}
Object:
#Data
#Document
public class Company {
public GeneralInfo info;
public Map<String, List<Employee>> officeIdEmployeeMap;
#Data
public class GeneralInfo {
#Id
public String companyId;
public String name;
}
#Data
public class Employee {
public String firstName;
public String lastName;
}
}
I need to get only GeneralInfo objects and then if some conditions are true get the List<Employee> from the officeIdEmployeeMap, not the whole map.
Can this be done by MongoRepository?
This is possible with MongoTemplate.
finalString officeName = "IT";
final Query query = new Query();
query.addCriteria(Criteria.where("officeIdEmployeeMap." + officeName).exists(true));
query.fields().exclude("officeIdEmployeeMap");
final Company company = mongoTemplate.findOne(query, Company.class, "collection");
This will return the Company object but only GeneralInfo will be set.
In CompanyRepository add the following method:
#Query(value="{}", fields = "{'info':1}")
public List<Company> getCompanies();
Call this when you need to get only General Info otherwise, call findAll() to get all details. You may write a new Query in the repository restricting the selected fields and optionally the criteria.
In case you want to achieve both the scenarios (get GeneralInfo only and get both) in the same query conditionally, instead of annotation create a query like below:
#Autowired
private MongoTemplate mongoTemplate;
Query query = new Query();
boolean someCondition = true; // Put your condition here
if(someCondition) {
query.fields().exclude("officeIdEmployeeMap");
}
return mongoTemplate.find(query, Company.class);
Assuming your company data in the database looks like this:
{ "_id" : ObjectId("5d3afb75b2ebf8a9ec2f906f"), "info" : { "companyId" : "1", "name" : "abc" }, "officeIdEmployeeMap" : { "g1" : [ { "firstName" : "A", "lastName" : "B" }, { "firstName" : "C", "lastName" : "D" } ] } }

Nested projection doesn't work with native query in Spring Data JPA

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#projections
In the reference, they only mention how to do nested projection with JPQL.
Assume I have these projection:
public interface ThreadWithContent {
Integer getId();
String getTitle();
UserSummary getAuthor();
}
public interface UserSummary {
Integer getId();
}
How can I query the Thread with projection using native query, I tried this:
#Query(value =
"select thread.id as id,thread.title as title,author.id as authorId "+
"from thread inner join users as author " +
"on thread.author_id = author.id " +
"where thread.id = ?1",nativeQuery = true)
ThreadWithContent getThreadsById(Integer threadID);
But it looks like Spring Data can only map the thread entity, but can't map the author enitity
{
"title": "Recusandae nihil fugiat deserunt.",
"author": null,
"id": 5
}
I have tried author.id as authorId, author.id as author_Id but none of them works.
I implemented this feature using constructor for nested projection.
Take into account that to call constructor you need to use class name together with package name
#Value("#{new com.mycompany.projections.BadgeProjection(target.badgeId, target.badgeName, target.badgeDescription, target.badgeImageUrl)}")
Complete example
Parent Projection
import org.springframework.beans.factory.annotation.Value;
import java.time.LocalDate;
public interface ChallengeProjection {
Long getId();
String getName();
String getDescription();
String getImageUrl();
LocalDate getStartDate();
LocalDate getEndDate();
#Value("#{new com.mycompany.projections.BadgeProjection(target.badgeId, target.badgeName, target.badgeDescription, target.badgeImageUrl)}")
BadgeProjection getBadge();
}
Nested Projection
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
#Getter
#Setter
#AllArgsConstructor
public class BadgeProjection {
private Long id;
private String name;
private String description;
private String getImageUrl;
}
Spring Repository
#Repository
public interface WorkoutGroupRepository extends JpaRepository<WorkoutGroup, Long> {
#Query(value = "select wg.id, wg.name, wg.description, wg.image_url as imageUrl, wg.start_date as startDate, wg.end_date as endDate," +
" b.id as badgeId, b.name as badgeName, b.description as badgeDescription, b.image_url as badgeImageUrl " +
" from workout_group wg left join badge b on b.id = wg.badge_id where wg.start_date between :weekStartDate and :weekEndDate"
, nativeQuery = true)
List<ChallengeProjection> getChallengeProjectionByWeek(#Param("weekStartDate") LocalDate weekStartDate, #Param("weekEndDate") LocalDate weekEndDate);
}
Example result
[ {
"description" : "Challenge description 1",
"name" : "Challenge name 1",
"id" : 1,
"imageUrl" : "myimage.jpeg",
"startDate" : "2020-12-15",
"endDate" : "2020-12-23",
"badge" : {
"id" : 1,
"name" : "Badge 1",
"description" : "Badge 1",
"getImageUrl" : "myimage.jpeg"
}
} ]

Spring Data Rest testing with JPA Relationship Mapping : Odd behaviour with update

I followed this tutorial (https://spring.io/guides/tutorials/react-and-spring-data-rest/#react-and-spring-data-rest-part-5) to experiment Spring Data REST and I wanted to test the CRUD with TestRestTemplate.
Add (postForEntity) is ok.
Delete (delete) is ok.
Read (getForEntity) is ok.
Update (template.exchange(URL, HttpMethod.PUT, entity, String.class, ID)) only works when I don't have any relation with other entities... and I don't understand why.
Here's an example :
#Data
#Entity
public class Dojo {
private #Id #GeneratedValue Long id;
private String name;
private String location;
private Date created;
#OneToMany(mappedBy = "dojo")
#JsonIgnore
private List<Workshop> workshops;
private Dojo() {}
public Dojo(String name, String location) {
this.name = name;
this.location = location;
this.created = new Date();
this.workshops = new ArrayList<>();
}
//getters and setters ...
}
#Data
#Entity
public class Workshop {
private #Id #GeneratedValue Long id;
private String name;
#ManyToOne
private Dojo dojo;
private Workshop() {}
public Workshop(String name, Dojo dojo) {
this.name = name;
this.dojo = dojo;
}
}
So, I have a bidirectionnal 1:n relation between Dojo & Workshop. The #JsonIgnore annotation is here to avoid an infinite loop with the JSON Marshaller.
The repositories are standard
public interface WorkshopRepository extends CrudRepository<Workshop, Long> {}
Now my test : I want to update a workshop. Sounds good, doesn't work.
#Test
public void testUpdateWorkshop() throws Exception {
final String DOJO_NAME="My Dojo";
final String DOJO_LOCATION="Liege";
final String WORKSHOP_NAME="Stuff";
final String HOST_PORT="http://localhost:8080";
//creation of a dojo
final Dojo DOJO = dojoRep.save(new Dojo(DOJO_NAME,DOJO_LOCATION));
//creation of a workshop
Workshop workshop = workshopRep.save(new Workshop(WORKSHOP_NAME,DOJO));
String newValue = "After Test";
System.out.println("before update");
System.out.println(workshop.getName()+" == "+WORKSHOP_NAME);
Long oldID = workshop.getId();
//As you can see I didn't modify the workshop object
HttpEntity<Workshop> entity = new HttpEntity<Workshop>(workshop);
ResponseEntity<String> response = template.exchange(HOST_PORT+"/api/workshops/"+oldID, HttpMethod.PUT, entity, String.class, oldID);
assert response.getStatusCodeValue() == 200;
//re-Get the updated workshop
workshop = workshopRep.findOne(oldID);
System.out.println("after update");
System.out.println(workshop.getName()+" == "+WORKSHOP_NAME);
// as I didn't set the newValue, it must fail and workshop.getName() must stay equal to "Stuff".
Assert.assertEquals("Update does not work",newValue,workshop.getName());
}
I run mvn clean test and
before update
Stuff == Stuff
after update
My Dojo == Stuff
Failed tests:
WorkshopTests.testUpdateWorkshop:218 Update not work expected:<[After Test]> but was:<[My Dojo]>
So basically, I didn't change anything into my object but
Result code is 200.
It changed a property of my object.
The name was modified to take the dojo.name value !
Just ... Why ?
More information :
When I create a new workshop object with a new name (using the newValue ;-) ) and a new Dojo and try to update the existing workshop, the result is still the same. workshop.dojo unchanged and name copied from dojo.name. So basically, my update doesn't work.
I also try with mockMvc instead of TestRestTemplate like this.
mockMvc.perform(put(HOST_PORT+"/api/workshops/"+oldID)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(convertObjectToJsonBytes(workshop))
);
with the function
private byte[] convertObjectToJsonBytes(Object object) throws IOException {
ObjectMapper mapper = new ObjectMapper();
System.out.println("log my face ");
System.out.println(mapper.writeValueAsString(object));
return mapper.writeValueAsBytes(object);
}
And the log seems to rightly parse my object before update...
{"id":1,"name":"Stuff","dojo":{"id":1,"name":"My Dojo","location":"Liege","created":1500799092330}}
but still doesn't work :(
When I run the app (mvn spring-boot:run), a GET on localhost:8080/api/workshops/1 returns
{
"name" : "Stuff",
"_links" : {
"self" : {
"href" : "http://localhost-core:8080/api/workshops/1"
},
"workshop" : {
"href" : "http://localhost-core:8080/api/workshops/1"
},
"dojo" : {
"href" : "http://localhost-core:8080/api/workshops/1/dojo"
}
}
}
If I change the property name of my Dojo class by nameD and I update with a new name and a new Dojo (previously saved into DB), the name is updated but not the dojo.
To summarize my questions are :
Just ... why ?
What is the correct way to update an object like Workshop with a HTTP request ?
What is the correct way to test this update ?
Thanks to all and have a nice day ! :-)
I think it's because you are using bidirectional one-to-many association. In this case you have to provide linking/unlinking of entities by yourself. For example in the collection setter, like this:
#Data
#ToString(exclude = "slaves")
#Entity
public class Master {
#Id
#GeneratedValue
private Long id;
private String name;
#OneToMany(mappedBy = "master", cascade = {PERSIST, MERGE})
private List<Slave> slaves;
public void setSlaves(List<Slave> slaves) {
// link new slaves to this master
slaves.forEach(slave -> slave.setMaster(this));
// unlink prev slaves
if (this.slaves != null) this.slaves.forEach(slave -> slave.setMaster(null));
this.slaves = slaves;
}
}
#Data
#Entity
public class Slave {
#Id
#GeneratedValue
private Long id;
private String name;
#ManyToOne
private Master master;
}
Then you can store Slave:
POST http://localhost:8080/api/slaves
{
"name": "slave1"
}
// the same for salve2, slave3, slave4
Store Master:
POST http://localhost:8080/api/masters
{
"name": "master1",
"slaves": [
"http://localhost:8080/api/slaves/1",
"http://localhost:8080/api/slaves/2"
]
}
Update Master:
PUT http://localhost:8080/api/masters/1
{
"name": "master1u",
"slaves": [
"http://localhost:8080/api/slaves/3",
"http://localhost:8080/api/slaves/4"
]
}
PUT http://localhost:8080/api/masters/2
{
"name": "master2"
}
Or update Slave:
PUT http://localhost:8080/api/slaves/1
{
"name": "slave1u",
"master": "http://localhost:8080/api/masters/2"
}
PUT http://localhost:8080/api/slaves/2
{
"name": "slave2u",
"master": "http://localhost:8080/api/masters/2"
}
See working example.
Additional info

Using Spring Data MongoDB to find and update document using compound _id field

I have a custom key class in my project as below.
I have implemented Serializable, implemented hashCode and equals too.
public class SalesKey implements Serializable {
Date timestamp;
String hostName;
public Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp=timestamp;
}
public void setHostName(String hostName) {
this.hostName=hostName;
}
public String getHostName() {
return hostName;
}
#Override
public int hashCode() {
..
}
#Override
public boolean equals(Object obj) {
...
}
}
I use the custom key class in my MongoDB Document class as below.
#Document(collection = "Sales")
public class Sales {
#Id
SalesKey salesKey;
//Other fields
}
I am able to save the documents into mongodb using spring data mongodb.
Sales sales=new Sales();
SalesKey salesKey=getSalesKey();
sales.setSalesKey(salesKey)
//set other fields for sales object
//Save sales Document object to mongodb
The Save is successful.
I am also able to fetch by using the below query in my SalesRepository interface which extends MongoRepository.
After constructing, the salesKey as before.
#Query(value = "{ '_id' : ?0 }")
public Sales filterBySalesKey(SalesKey salesKey);
When I try to update using a criteria API as below, for updating specific fields in the document.
I first use the Criteria as below.
Criteria salesKeyCriteria = Criteria.where("_id").is(salesKey);
With the above criteria I then execute a update statment,
But, my update fails with the error,
org.bson.codecs.configuration.CodecConfigurationException: Can't find a codec for class com.demo.model.SalesKey
I am using Spring Data MongoDB framework and MongoDB 3.2.1.
Can anyone help me what is the codec and how can I add one to make my update successful.
Since SalesKey is your Mongo _id, you could do the following with MongoTemplate:
#Autowired
private MongoTemplate mongoTemplate;
...
Query query = new Query(Criteria.where("_id").is(salesKey));
mongoTemplate.updateFirst(query,Update.update("field", "new-value"),Sales.class);
Following up on your comment on mongo explain
If I look at the update explain in mongo shell:
$ db.sales.explain().update({"_id" : { "timestamp" : ISODate("2016-08-25T12:12:59.251Z"), "hostName" : "myhostname" }},{value:"new-value"})
It shows me the winning plan using IDHACK:
"winningPlan" : {
"stage" : "UPDATE",
"inputStage" : {
"stage" : "IDHACK"
}
},
Example of a Sales document:
{ "_id" : { "timestamp" : ISODate("2016-08-25T12:12:59.251Z"), "hostName" : "value" }, "value" : "new" }