We have to query data from database where we need to find entities matching a list of key value pairs. We thought it would be a nice idea to use Spring Data JPA as we need also pagination.
The tables we created are like below:
terminal(ID,NUMBER,NAME);
terminal_properties(ID,KEY,VALUE,TERMINAL_FK);
Is it possible to define a query method to fetch all terminals with properties containing given key/value pairs ?
Something like this: List<Terminal> findByPropertiesKeyAndValue(List<Property>);
I didn't execute the code, but given the correct import statements, this at least compiles. Depending on your entity definition, some properties may need to be adapted and in any case, you should get an idea of how to approach this.
My criteria query is based on the following SQL:
SELECT * FROM TERMINAL
WHERE ID IN (
SELECT TERMINAL_FK FROM TERMINAL_PROPERTIES
WHERE (KEY = 'key1' AND VALUE = 'value1')
OR (KEY = 'key2' AND VALUE = 'value2')
...
GROUP BY TERMINAL_FK
HAVING COUNT(*) = 42
)
Where you list each name/value pair and 42 simply represents the number of name/value pairs.
So I assume you defined a repository like this:
public interface TerminalRepository extends CrudRepository<Terminal, Long>, JpaSpecificationExecutor {
}
It's important to extend JpaSpecificationExecutor in order to make use of the criteria API.
Then you can build a criteria query like this:
public class TerminalService {
private static Specification<Terminal> hasProperties(final Map<String, String> properties) {
return new Specification<Terminal>() {
#Override
public Predicate toPredicate(Root<Terminal> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
// SELECT TERMINAL_FK FROM TERMINAL_PROPERTIES
Subquery<TerminalProperty> subQuery = query.subquery(TerminalProperty.class);
Root propertyRoot = subQuery.from(TerminalProperty.class);
subQuery.select(propertyRoot.get("terminal.id"));
Predicate whereClause = null;
for (Map.Entry<String, String> entry : properties.entrySet()) {
// (KEY = 'key1' AND VALUE = 'value1')
Predicate predicate = builder.and(builder.equal(propertyRoot.get("key"),
entry.getKey()), builder.equal(propertyRoot.get("value"), entry.getValue()));
if (whereClause == null) {
whereClause = predicate;
} else {
// (...) OR (...)
whereClause = builder.or(whereClause, predicate);
}
}
subQuery.where(whereClause);
// GROUP BY TERMINAL_FK
subQuery.groupBy(propertyRoot.get("terminal.id"));
// HAVING COUNT(*) = 42
subQuery.having(builder.equal(builder.count(propertyRoot), properties.size()));
// WHERE ID IN (...)
return query.where(builder.in(root.get("id")).value(subQuery)).getRestriction();
}
};
}
#Autowired
private TerminalRepository terminalRepository;
public Iterable<Terminal> findTerminalsWith(Map<String, String> properties) {
// this works only because our repository implements JpaSpecificationExecutor
return terminalRepository.findAll(hasProperties(properties));
}
}
You can obviously replace Map<String, String> with Iterable<TerminalProperty>, although that would feel odd because they seem to be bound to a specific Terminal.
Related
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.
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 am getting an array-list as a return type with 0th index as an array. What i want is a key value pair segregation, where column name is the key and its value is the corresponding sum i obtained.
#Query(value = "
SELECT SUM(revenue_amount) AS revenue_amount
, SUM(service_charge_amount) AS service_charge_amount
FROM event_stats
WHERE event_id = :event_id
", nativeQuery = true)
public ? findTicketTotalForAnEvent(#Param("event_id") String event_id);
What should be the return type ? And how to filter it further ?
If you're open to use Projections, something like below would give you desired result.
Declare a static interface like below:
static interface TotalTicketsForEvent {
public Long getRevenueAmount();
public Long getServiceChargeAmount();
}
Now your query should be:
#Query(value="SELECT SUM(revenue_amount) AS revenueAmount, SUM(service_charge_amount) AS serviceChargeAmount FROM event_stats WHERE event_id = :event_id", nativeQuery = true)
public List<TotalTicketsForEvent> findTicketTotalForAnEvent(#Param("event_id") String event_id);
You can use the interface methods to process the list further.
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've created a Repository that extends CrudRepository,
this repository has a method with an #Query notation:
Code:
#Query("select itemType, count(*) as count from Item where User_id = :userId group by itemType")
List<Map<String, Long>> countItemsForUser(#Param("userId") Long userId);
The issue I'm having is that this return a ArrayList of Object(s) and not a List of Map.
I've read somewhere that JPA can't return a Map so that's why I stuff the result in a List>.
I don't know what's the best way to work around this issue or to quickly access the result data.
I've tried casting but that didn't work out either:
for(Object item: items) {
Map<String,Long> castedItem = (HashMap<String,Long>)item;
}
See this example in official documentation of Hibernate.Here
for (Object item:items) {
Object[] tuple = (Object[]) item;
String itemType = (String)tuple[0];
Long count = (Long) tuple[1];
}
Most simple way is to use interface. To let Spring wire query alias
to the interface getter. Example can be found here: https://www.baeldung.com/jpa-queries-custom-result-with-aggregation-functions
also there is #SqlResultSetMapping. See:
JPA- Joining two tables in non-entity class