I want to map cypher query results to a DTO/POJO class. I have the following entities defined in neo4j:
Products , which has properties; Name, Title, Address
Sellers, which has properties; Name, Id
Listings, which has properties; Name, Id
Relationshps are defined as: Products -> Sellers & Sellers -> Listings
My query results is List of Product.Name, [ {Listings.Name, Listings.Id, Sellers.Id, Sellers.Name} ].
I wish to map this to a DTO, I am not able map this result which has different nodes and labels to a DTO/POJO class.
As you have already noticed, Spring Data Neo4j is more strict when it comes to map "arbitrary" data that is not directly applicable to one domain entity.
But on the other hand Spring Data Neo4j also offers support for mapping loose data with the Neo4jClient.
Example:
class SoldProductInformation {
String productName;
Set<SellingInformation> sellingInformation;
}
class SellingInformation {
String listingsName;
String listingsId;
String sellerName;
String sellerId
}
neo4jClient.query("...return product.name as productName, someListWithTheInformationFromTheQuestion")
.fetchAs(SoldProductInformation.class)
.mappedBy((TypeSystem t, Record record) -> {
String productName = record.get("productName").asString();
List<SellingInformation> sellingInformations = record.get("someListWithTheInformationFromTheQuestion").asList(value -> {
String listingsName = value.get("listingsName").asString();
// same for listingsId, sellerName, sellerId...
return new SellingInformation(....);
});
return new SoldProductInformation(....);
})
If you have more entity aligned fields and/or maybe return also nodes, you can make use of the derived mapping function:
BiFunction<TypeSystem, MapAccessor, Product> mappingFunction = neo4jMappingContext.getRequiredMappingFunctionFor(Product.class);
and apply it via
neo4jClient.query("...return product,...")
.fetchAs(SoldProductInformation.class)
.mappedBy((TypeSystem t, Record record) -> {
Product product = mappingFunction.apply(typeSystem, record.get("product"));
String productName = product.getName();
// ....
see https://github.com/spring-projects/spring-data-neo4j/issues/2288#issuecomment-861255508 for a complete example.
Related
So I'm building a group of dropdowns that rely upon each other and built a query to get the code and description for a Product Type, Family, and Model object. I used nested hashmaps to story all of the data and objects. This was fine because I can just call all of the information that I need from the hashmaps. However, when it comes to the REST API's, it's going to display all of the nested information for each of the hashmaps when I call them. For each map I have it's key, and then the value consists of a Code, Desc, and the hashmap of the next object.
So, it would be like:
Main hashmap
- Key
- value
-> code
-> desc
-> product family hashmap
-- key
-- value
--> code
--> desc
--> product model hashmap
--- key
--- value
---> code
---> desc
My main question is how can I either strip these additional hashmaps from being displayed in the json format when viewing the REST API via web browser? Or can/do I need to just completely strip the additional information altogether?
#Service
public class ProductDAOImpl implements ProductDAO {
#PersistenceContext
private EntityManager em;
#Override
public Map<String, ProductType> getProductTypeStructure() {
HashMap<String, ProductType> prodTypes = new HashMap<>();
Query q = em.createNativeQuery("<query>");
List<Object[]> prodTypeEntities = q.getResultList();
final String badData = "XX-BAD-XX";
ProductType prodType = new ProductType(badData, "");
ProductFamily prodFamily = new ProductFamily(badData, "");
for(Object[] prodTypeEntity : prodTypeEntities) {
if (prodTypeEntity[1] == null || prodTypeEntity[3] == null || prodTypeEntity[5] == null) {
continue;
}
String prodTypeCd = prodTypeEntity[0].toString().toUpperCase();
String prodTypeDesc = StringUtils.trimTrailingWhitespace(prodTypeEntity[1].toString()).toUpperCase();
String prodFamilyCd = prodTypeEntity[2].toString().toUpperCase();
String prodFamilyDesc = StringUtils.trimTrailingWhitespace(prodTypeEntity[3].toString()).toUpperCase();
String prodModelCd = prodTypeEntity[4].toString().toUpperCase();
String prodModelDesc = StringUtils.trimTrailingWhitespace(prodTypeEntity[5].toString()).toUpperCase();
if(!prodType.getCode().equalsIgnoreCase(prodTypeCd)) {
prodType = new ProductType(prodTypeCd, prodTypeDesc);
prodType.setProdFamilies(new HashMap<String, ProductFamily>());
prodTypes.put(prodType.getCode(), prodType);
prodFamily.setCode(badData);
}
if(!prodFamily.getCode().equalsIgnoreCase(prodFamilyCd)) {
prodFamily = new ProductFamily(prodFamilyCd, prodFamilyDesc);
prodFamily.setProdModels(new HashMap<String, ProductModel>());
prodType.getProdFamilies().put(prodFamily.getCode(), prodFamily);
}
prodFamily.getProdModels().put(prodModelCd, new ProductModel(prodModelCd, prodModelDesc));
}
return prodTypes;
}
}
If I understood your question correctly, I think a DTO object might be the answer here. You add to it only the values that the dropdown might need and return it from the REST API.
Here's more on DTOs.
I have an interesting problem that I want to solve. This is while I am parsing a response from one of the platforms that we interact with. The response changes based on the User.
Say for User A, I have the following JSON :
{
"userId": "AA001",
"AA001_Name": "Username",
"AA001_Email": "user#gmail.com",
"AA001_Phone": "000-000-0000"
}
For User B, I have :
{
"userId" : "AA002",
"AA002_Name" : "Username",
"AA002_Email" : "user#gmail.com",
"AA002_Phone" : "000-000-0000"
}
Now, while deserializing, I want to map both of them to the following object, ignoring the field name the json came with :
class User {
private String userId,
private String name,
private String email,
private String phone
}
It is easy to map the userId, as that's the field in the JSON as well, but what about the custom fields?
Now, I can't use the #JsonProperty as the name of the field is dynamically changing based on the user.
Is there any way this could be accomplished?
Please note that I might have several such custom objects like Department, Organization etc, and the platform returns the data in such a manner, meaning the keys have the user-specific information appended.
Any help is appreciated. I am badly stuck at this.
I think you can't do any better than using #JsonCreator:
class User {
private String userId;
private String name;
private String email;
private String phone;
#JsonCreator
public User(Map<String, Object> map) {
this.userId = (String) map.get("userId");
map.entrySet().stream()
.filter(e -> e.getKey().endsWith("_Name"))
.findFirst()
.ifPresent(e -> this.name = (String) e.getValue());
// repeat for other fields
}
// getters & setters (if needed)
}
You can change the stream by a traditional for each on the map's entry set to optimize performance-wise.
You can't use #JSONProperty as is, but what if you reformatted the keys before deserializing the JSON? I.E
String key = "AA001_Name"
String[] key = key.split("_")
key[0] = "AA001"
key[1] = "Name"
//Use key[1] as the new field name
Do this for each key, create a new JSON Object with the correct field names, then deserialize it.
I have following query which i use through #Query annotation with GraphRepository in spring data neo4j. So to get a result i declare return type of method as List
#Query(value = "START user=node:searchByMemberID(memberID=1) MATCH user-[r:FRIENDS_WITH]->member RETURN member")
List<Node> getNodes(int userID);
Now if i want to write a query which returns 2 columns, what will be the return type of its corresponding method. For e.g. what should i write in place of List, as in above query, for the below mentioned query.
START user=node:searchByMemberID(memberID='1') MATCH user-[r:FRIENDS_WITH]->member RETURN member, r.property
In that case queries return an Iterable<Map<String,Object>> which allows you to iterate over the returned rows. Each element is a map which you can access by the name of the returned field and using the neo4jOperations conversion method to cast the value object to its proper class, i.e.:
Iterable<Map<String, Object>> it = getNodes(...);
while (it.hasNext()) {
Map<String, Object> map = it.next();
obj = neo4jOperations.convert(map.get("member"), Node.class);
...
}
You can now do something like this in SDN:
#QueryResult
public class NodeData {
Member member;
String property;
}
And then in the repository:
#Query("The Query...")
public NodeData getNodes(int userId);
This was adapted from an example in the documentation here: https://docs.spring.io/spring-data/neo4j/docs/4.2.x/reference/html/#reference_programming-model_mapresult
I have a list of Order objects -
class Order {
Date date;
float amount;
String companyCode;
}
List<Order> orders = /* Initialize with list of order objects with valid data */
I have a list of Company objects -
class Company {
String name;
String code;
String address;
}
List<Company> companies = /* Initialize with list of company objects with valid data */
I need a to create a map of companyCode and name.
Is there some library that would allow me to write code like this (where BeanSearch is the hypothetical library class)?
Map<String, String> codeAndName = new HashMap<String, String>();
for(Order o: orders) {
codeAndName.put(o.getCompanyCode(),
BeanSearch.find(companies, "code", o.getCompanyCode).getName());
}
Alternatively is there another good way to do it?
http://commons.apache.org/collections/apidocs/org/apache/commons/collections/CollectionUtils.html should work for you right? Specifically you can use the find method
I want to display list of categories in my Jsp page using spring mvc #modelAttribute.
In my mapper.xml file is
<select id="selectAllCategories" resultMap="BaseResultMap">
select id, name from categories
</select>
In my Mapper.java class I have method
List<Map<String, String>> selectAllCategories();
I want to have a method like this:
Map<Integer, String>`selectAllCategories();
instead of List<Map<>>, is that possible?
You want to get a Map<Integer,String> where the Integer is the id and the String is the name. If there were 200 categories in your table, you would want 200 entries in your map, rather than a list of 200 maps.
MyBatis can't quite do that out of the box, but you can use its facilities to do that. I see two options.
Option 1:
The first isn't quite what you asked for but is worth showing. It gives you a Map<Integer,Category> where Category is a domain object for the categories table that has id, name (and possibly other fields from the categories table). After you've created the Category domain object, this is quite easy to do in MyBatis using the #MapKey annotation:
#Select("SELECT id, name FROM categories")
#MapKey("id")
Map<Integer,Category> getAllCategories();
In your code you would then do:
MyMapper mapper = session.getMapper(MyMapper.class);
Map<Integer,Category> m = mapper.getAllCategories();
That may or may not work for your use case depending on whether whether you can extract the name as a property of the Category object.
Option 2:
To get the Map<Integer,String> you asked for, the easiest way I know is to create a class that implements the MyBatis ResultHandler interface.
Your ResultHandler will use the default hashmap of column-name => column-value that MyBatis creates and create a single master Map. Here's the code:
public class CategoryResultHandler implements ResultHandler {
Map<Integer,String> inMap = new HashMap<Integer,String>();
public Map<Integer, String> getIdNameMap() {
return inMap;
}
#Override
public void handleResult(ResultContext rc) {
#SuppressWarnings("unchecked")
Map<String,Object> m = (Map<String,Object>)rc.getResultObject();
inMap.put((Integer)getFromMap(m, "id"),
(String)getFromMap(m, "name"));
}
// see note at bottom of answer as to why I include this method
private Object getFromMap(Map<String, Object> map, String key) {
if (map.containsKey(key.toLowerCase())) {
return map.get(key.toLowerCase());
} else {
return map.get(key.toUpperCase());
}
}
}
The handleResult method gets called once per row in the category table. You tell MyBatis to use the ResultHandler and then extract your master map like this:
CategoryResultHandler rh = new CategoryResultHandler();
session.select("getAllCategories", rh);
Map<Integer,String> m = rh.getIdNameMap();
One of those two should work for you.
A few final notes:
Why did I include the getFromMap() helper method? Because you can't always control the case of the column name in the hashmap that MyBatis returns. More details here: mybatis- 3.1.1. how to override the resultmap returned from mybatis
I have working examples of these solutions in Koan26 of the mybatis-koans (which I added based on your question): https://github.com/midpeter444/mybatis-koans