mongodb query with spring data for key-value - java

I've been started a project with mongodb and spring boot and spring JPA data and I realised I cannot map my data model to entity and make a query on that easily, so I have two questions,
My data model is like that ( just for one Collection )
{
name: "Name",
lastName: "Last Name",
attributes: {
age: 25
eye: {
color: "RED",
size: "BIG"
}
}
}
And my entity is
#Entity // or #Document
public class User{
private String name;
private String lastName;
private Map<String, ?> attributes = new HashMap<>();
// id or the setter getter are omitted
}
Can I map attributes property in my mongodb collection like I did ( Map )
How can I make query for finding the attributes?
Can I do it like that?
List<User> findAllByAttributesAge(Integer age);

Today I had a problem with Map query in Spring Mongodb, and since this question pops the first one in google I will provide another answer.
Since the other answer references to documentation, and that documentation does not have a lot of information, I am going to put an example of how good is the #Query annotation for dynamic schema:
#Query(value = "{'attributes.age' : ?0}")
List<User> findAllByAttributesAge(int age);
Also, you could query eye color, too:
#Query(value = "{'attributes.eye.color' : ?0}")
List<User> findAllByAttributesEyeColor(String color);
As the other answers documentation says, you could filter the result, and receive only the part of the document that you prefer:
// It will return the users with only name and last name
#Query(value = "{'attributes.age' : ?0}", fields = "{ name : 1, lastName : 1 }")
List<User> findAllByAttributesAge(int age);

Can I map attributes property in my mongodb collection like I did ( Map )
Yes, you can, and it may prove useful (though tricky) for a "dynamic" or "partly defined" schema.
If you know your schema in advance, I strongly recommend that it shows in your domain object. Take a look here.
How can I make query for finding the attributes?
If you don't use a dynamic schema, property traversal for nested properties is clearly explained in the Spring Data MongoDB Reference Documentation.
If you use dynamic schema, you'll most probably use MongoDB JSON based query methods.

This gave me a tough time. The other answer pretty much helps with querying an object by the field value of an hashmap if you know the key for the field. But what if you don't know the key by which the object will be queried? I labored for hours trying to figure out what's wrong even after I followed Noki's very good explanation exactly.
In my case, I have a Customer class that has a hashmap field to store account objects.
public class Customer {
private String firstName;
private String lastName;
private Map<String, Account> customerAccounts = new HashMap<>();
//Constructors, getters, setters...
}
A data Model collection looks something like this
{
firstName: "firstname",
lastName: "lastname",
customerAccounts: {
"0123456789": {
firstName: "firstName",
lastName: "lastname",
accoutNumber: "0123456789",
accountType: "Savings"
},
"1012234494": {
firstName: "firstname",
lastName: "lastname",
accoutNumber: "1012234494",
accountType: "Current"
}
}
}
So, I wanted to query the customer from the database by the accountNumber field of the account object inside customerAccounts. Since the keys for every account in each customerAcoounts object in any Customer is unique and dynamic, and no Customer collection has a similar accountNumber with another Customer collection, you'll need to write your query like this:
#Query(value = "{'customerAccount.?0.accountNumber' : ?0}"
Optional<Customer> findCustomerByCustomerAccount(String accountNumber);
The ?0 in 'customerAccount.?0.accountNumber' will take care of the dynamic value, find an object that its key matches exactly the argument coming in and finally check the accountNumber field for that account object.

Related

Distinct values ignoring a field in Spring Data Mongodb

In my spring boot app utilizing mongodb I am trying to get a list of distinct nested objects.
The document looks like this (other details removed fore brevity):
class Movie {
otherproperties...;
List<CastRoleLink> cast;
...
}
//structure of CastRoleLink
class CastRoleLink {
String name;
String urlName;
String roleID;
}
//additional class for structure of results
class CastLink {
String name;
String urlName;
}
What I really want is a list of all the unique CastLink from all of the Movies matching my criteria, which means that I need all of the distinct CastRoleLink objects without the roleID. The tricky part is that for different movies I could have the same name and urlName properties but a different roleID property. In these cases I would consider them the same because the CastLink would be the same. My current solution is close but not quite right.
//query is Query object with criteria for Movies
mongoTemplate.findDistinct(query, "cast", Movies.class, CastLink.class);
This gives me duplicates when the name and urlName properties are the same but the roleID property is different. Is there a way that I can find distinct name and urlName objects while ignoring the roleID property?
I figured it out. The key is to use Aggregation.
Aggregation aggregation = newAggregation(
match(criteria),
unwind("cast"),
group("cast.urlName").addToSet("cast.name").as("name").addToSet("cast.urlName").as("urlName")
project("name").and("urlName").previousOpertaion(),
sort(Sort.Direction.ASC, "name")
)
return mongoTemplate.aggregate(aggregation, Movie.class, CastLink.class).getMappedResults();

Unable to map JSON field from postgres DB to POJO class

I have below code in place where I am trying to get only 1 column (JSON type) from postgres DB and map it to a POJO class. But, in the result I am getting correct count but with null values only, even though data is present. Any Help/Suggestion is appreciated.
POJO Class:
#JsonIgnoreProperties(ignoreUnknown = true)
public class EmployeePayload {
private String empName;
private String empID;
//getters and setters
}
Query Execution Code ::
String query = "SELECT emp_payload FROM EmpDetails";
ResultSetHandler<List<EmployeePayload>> employeePayload = new BeanListHandler<EmployeePayload>(EmployeePayload.class);
List<EmployeePayload> resultList = runner.query(dbConnection, query, employeePayload);
log.info(":: resultList is :: " + resultList);
Data in DB ::
"{""empName"":""james"",""empID"":""008""}",
"{""empName"":""bond"",""empID"":""007""}"
Result in Log ::
resultList is :: [EmployeePayload{empName='null', empID='null'}, EmployeePayload{empName='null', empID='null'}]
The BeanListHandler comes from apache dbutils (docs)
By looking at the docs / source, it's implemented by mapping single columns to single properties of the Bean. You are essentially trying to map a single column to multiple properties which does not work. Now there are two ways to go about this:
Writing your own RowProcessor
Rewriting the query to return multiple columns.
In this situation I would favor the second solution for it's simplicity as postgres has this functionality built-in for its json field types.
Your query could look like this:
String query = "SELECT emp_payload->'empName' AS empName, emp_payload->'empID' AS empID FROM EmpDetails";
(The AS ... might not be necessary but I don't know how psql generates the column names for extracted json values).
If you would execute this query directly in postgres, you would get a result set with column names empName and empID which is exactly what the BeanProcessor (the default processor for BeanListHandler) expects.

JDBC Template - One-To-Many

I have a class that looks like this. I need to populate it from two database tables, which are also shown below. Is there any preferred way to do this?
My thought is to have a service class to select a List<> via a ResultSetExtractor from a DAO. Then do a foreach on that list, and select a List<> of emails for the individual person via another ResultSetExtractor, and attach it from with the foreach loop.
Is there a better way, or is this as good as it gets?
public class Person {
private String personId;
private String Name;
private ArrayList<String> emails;
}
create table Person (
person_id varchar2(10),
name varchar2(30)
);
create table email (
person_id varchar2(10),
email varchar2(30)
);
This is best solved by an ORM. With JDBC, you have to do by hand what an ORM would do for you. Executing N + 1 queries is very inefficient. You should execute a single query, and build your objects manually. Cumbersome, but not hard:
select person.id, person.name, email.email from person person
left join email on person.id = email.person_id
...
Map<Long, Person> personsById = new HashMap<>();
while (rs.next()) {
Long id = rs.getLong("id");
String name = rs.getString("name");
String email = rs.getString("email");
Person person = personsById.get(id);
if (person == null) {
person = new Person(id, name);
personsById.put(person.getId(), person);
}
person.addEmail(email);
}
Collection<Person> persons = personsById.values();
I was looking for something similar, and although the answer is perfectly valid I went with this nice library instead https://simpleflatmapper.org/0203-joins.html
It also integrates perfectly with Spring boot.
main advantage is that you have a clean repository layer, it uses your pojo and makes refactoring much easier, and like hibernate you can still map deep nested and complex one to many and still be in control of what is executed.
It also has a nice jdbctemplate CRUD and Java 13 finally brings support for multi-line string literals which is very good for sql statements readability. hope this helps someone :)
In my case, I had to use the LinkedHashMap to keep the query result ordered by the position field.
From JavaDoc:
LinkedHashMap: "This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map (insertion-order). Note that insertion order is not affected if a key is re-inserted into the map."
HashMap: "This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time".
TIP: using the getOrDefault method eliminates the extra check for nullable object.
public List<BucketDto> findAll() {
var sql = """
SELECT
b.uuid bucket_uuid, b.position bucket_position, b.name bucket_name,
c.uuid card_uuid, c.position card_position, c.name card_name
FROM bucket AS b
LEFT JOIN card AS c ON c.bucket_id = b.id
ORDER BY b.position ASC, c.position ASC
""";
return jdbcTemplate.query(sql, rs -> {
Map<Double, BucketDto> resultMap = new LinkedHashMap<>();
while (rs.next()) {
var position = rs.getDouble("bucket_position");
var bucketDto = resultMap.getOrDefault(position, new BucketDto(
UUID.fromString(rs.getString("bucket_uuid")),
position,
rs.getString("bucket_name")));
if (Optional.ofNullable(rs.getString("card_uuid")).isPresent()) {
bucketDto.addCard(new CardDto(
UUID.fromString(rs.getString("card_uuid")),
rs.getDouble("card_position"),
rs.getString("card_name")));
}
resultMap.put(position, bucketDto);
}
return new ArrayList<>(resultMap.values());
});
}

JPA Query.getResultList() - use in a generic way

I'm creating a complex query with multiple tables and need to list the result. Usually, I'm using the EntityManager and map the result to the JPA-Representation:
UserEntity user = em.find(UserEntity.class, "5");
Then I can access all values as the user UserEntity class defines it. But how can I access the field-values returned from a native, multiple-table query? What I get is a List of Objects. That's fine so far, but what "is" that Object? Array? Map? Collection? ...
//simpleExample
Query query = em.createNativeQuery("SELECT u.name,s.something FROM user u, someTable s WHERE s.user_id = u.id");
List list = query.getResultList();
//do sth. with the list, for example access "something" for every result row.
I guess the answer is quite simple, but most examples out there just show the usage when directly casting to a targetClass.
PS: In the example I could use the class-mappings of course. But in my case someTable is not managed by JPA, and therefore I don't have the entity nor do I have a class-representation of it, and since I'm joining like 20 tables, I don't want to create all the classes just to access the values.
General rule is the following:
If select contains single expression and it's an entity, then result is that entity
If select contains single expression and it's a primitive, then result is that primitive
If select contains multiple expressions, then result is Object[] containing the corresponding primitives/entities
So, in your case list is a List<Object[]>.
Since JPA 2.0 a TypedQuery can be used:
TypedQuery<SimpleEntity> q =
em.createQuery("select t from SimpleEntity t", SimpleEntity.class);
List<SimpleEntity> listOfSimpleEntities = q.getResultList();
for (SimpleEntity entity : listOfSimpleEntities) {
// do something useful with entity;
}
If you need a more convenient way to access the results, it's possible to transform the result of an arbitrarily complex SQL query to a Java class with minimal hassle:
Query query = em.createNativeQuery("select 42 as age, 'Bob' as name from dual",
MyTest.class);
MyTest myTest = (MyTest) query.getResultList().get(0);
assertEquals("Bob", myTest.name);
The class needs to be declared an #Entity, which means you must ensure it has an unique #Id.
#Entity
class MyTest {
#Id String name;
int age;
}
The above query returns the list of Object[]. So if you want to get the u.name and s.something from the list then you need to iterate and cast that values for the corresponding classes.
I had the same problem and a simple solution that I found was:
List<Object[]> results = query.getResultList();
for (Object[] result: results) {
SomeClass something = (SomeClass)result[1];
something.doSomething;
}
I know this is defenitly not the most elegant solution nor is it best practice but it works, at least for me.
Here is the sample on what worked for me. I think that put method is needed in entity class to map sql columns to java class attributes.
//simpleExample
Query query = em.createNativeQuery(
"SELECT u.name,s.something FROM user u, someTable s WHERE s.user_id = u.id",
NameSomething.class);
List list = (List<NameSomething.class>) query.getResultList();
Entity class:
#Entity
public class NameSomething {
#Id
private String name;
private String something;
// getters/setters
/**
* Generic put method to map JPA native Query to this object.
*
* #param column
* #param value
*/
public void put(Object column, Object value) {
if (((String) column).equals("name")) {
setName(String) value);
} else if (((String) column).equals("something")) {
setSomething((String) value);
}
}
}
What if you create a bean with all required properties and cast the result using Java 8+ streams?
Like this:
public class Something {
private String name;
private String something;
// getters and setters
}
And then:
import javax.persistence.Query;
...
Query query = em.createNativeQuery("SELECT u.name,s.something FROM user u, someTable s WHERE s.user_id = u.id", Something.class);
List<?> list = query.getResultList();
return list
.stream()
.map(item -> item instanceof Something ? (Something) item : null)
.collect(Collectors.toList());
That way, you don't need to return List<Object[]> nor hide the warning with #SuppressWarnings("unchecked")
Ps.:
1 - I know that this post is very old. But... I'm here in 2021, so others will be coming here too =)
2 - This is wrong or bad practice? Let me know :D
You can also update your hibernate to a version greater than 5.4.30.final

Morphia to return List of strings which are the fields of all documents

If I got a collection full of following elements
#Entity
public void MyEntity{
public String name;
public String type;
...
}
And I want to return a List<String> (or Set) of not the elements, but only their name fields.
List<String> allNames = datasotre.find(MyEntity.class).asList("name");
This is sample query, there is no such method of Morphia datastore.
To limit the fields returned call the "retrievedFields" method on Query. For example, to only get the name field of all MyEntity objects:
datastore.find(MyEntity.class).retrievedFields( true, "name").asList()
Edit - You can get a list Strings using the following query as long as you don't mind that the list will only contain unique values (i.e. no duplicate names):
DBCollection m = datastore.getCollection( MyEntity.class );
List names = m.distinct( "name", new BasicDBObject() );
The "names" list will only contain Strings.
The problem here is that there is no actual query for "keys". The queries all return "key/value pairs".
In theory, the fields in datastore.find() should map to the fields in MyEntity so you could just use reflection. However, if you have other people writing to the DB from different places they may have seeded extra tables.
If this is the case, you will need to run a Map/Reduce to get the list of all the "key" names.
There is a sample here.
You are looking for datastore.find(MyEntity.class).retrievedFields(true, "name").asList();. This will contain the _id and name attribute.
See http://code.google.com/p/morphia/wiki/Query#Ignoring_Fields

Categories