JPA AttributeConverter search predicate in specification - java

I am trying to have a class that has a certain list of objects (specified by another class) persisted in the database as a string (use JPA Converter - all good).
And then I want to use Specification to search inside that string.
What is the best way to create the predicates? I don't seem to understand the connection bettween the AttributeConverter and the Expression in the Specification.
The parent class:
#Entity #Table
public class A {
#Column #Id #GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#Column
private String name;
#Enumerated(EnumType.STRING)
private SomeType type;
#Column(length=1000) #Convert(converter = BConverter.class)
private List<B> bList;
private Integer no;
}
The listed object class:
public class B{
private String type;
private Integer quantity;
}
The converter:
#Converter
public class BConverter implements AttributeConverter<List<B>, String> {
private static final String SEPARATOR = ":";
private static final String LIST_SEPARATOR = ";";
#Override public String convertToDatabaseColumn(List<B> bList) {
return bList.stream().map(e->convertToString(e)).collect(Collectors.joining(LIST_SEPARATOR));
}
#Override public List<B> convertToEntityAttribute(String str) {
if(str==null || str.isEmpty() ) return null;
return Arrays.stream(str.split(LIST_SEPARATOR)).map(e->convertFromString(e)).collect(Collectors.toList());
}
private String convertToString(B b){
if(entity==null) return null;
return b.getType().toString() +SEPARATOR+ b.getQuantity().toString();
}
private B convertFromString(String subStr){
if(subStr==null || subStr.isEmpty() ) return null;
String[] pair = subStr.split(SEPARATOR);
return new B(pair[0],Integer.valueOf(pair[1]));
}
}
In the database should look something like:
Table A:
id: 1;
name: "Some Name";
type: "THISTYPE";
blist: "TYPE1:11;TYPE2:22";
no: 0;
id: 2;
name: "Other Name";
type: "THISTYPE";
blist: "TYPE1:45;TYPE2:56";
no: 12;
I would then like to create Specifications to search over this table for the attributes inside the bList.
For example, search by an entity that contains a B object where type=TYPE1 and a quantity>=30.
public static Specification<A> customSpecification(String type, Integer value) {
return (root, query, builder) -> ///?????????
}
Is there a way to use such specifications where the DB attribute is a String but JAVA only sees the objects?

Related

java.lang.NullPointerException due to 'Map<String, Integer>' may not contain keys of type 'Path<String>'

I am facing an issue in a springboot project,
I am trying to retrieve statistics of "Tickets" that are handled ontime using jpa specifications.
Ticket are given a number of days to handle based on the purpose.
Here is the error:
java.lang.NullPointerException: null
at com.ticketcorp.ticket.repository.TicketSpecification.lambda$isOntime$c9c337fb$1(TicketSpecification.java:208) ~[classes/:na]
Which i Believe is to be expected since i got this warning on the same line:
'Map<String, Integer>' may not contain keys of type 'Path<String>'
Here is my Ticket Entity:
#Table(name = "tickets")
public class Ticket {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String code;
private String purpose;
#Lob
#Column(columnDefinition = "TEXT")
private String content;
#Lob
#Column(columnDefinition = "TEXT")
private String solution;
#Lob
#Column(columnDefinition = "TEXT")
private String comment;
private int status;
private LocalDateTime createdAt= LocalDateTime.now();
private LocalDateTime handledAt= LocalDateTime.now();
}
Here is my Ticket Specification:
public class TicketSpecification {
public static Specification<Ticket> isOntime(ArrayList<Purpose> purposes) {
return (root, query, builder) -> {
/*Example of content for nameAndDurationMap: {Suggestion=25, Bug report=1}*/
Map<String, Integer> nameAndDurationMap = PurposeUtils.PurposeArrayToNameDurationMap(purposes);
return builder.le(
builder.diff(
builder.function("unix_timestamp", Long.class, root.get(Ticket_.handledAt)),
builder.function("unix_timestamp", Long.class, root.get(Ticket_.createdAt))
)
, nameAndDurationMap.get(root.get(Ticket_.purpose)) * 86400);/*Line 208*/
};
}
}
Here is my Ticket Service:
#Service
public class TicketService {
#Autowired
private TicketRepository ticketRepository;
public String countTicketsHandledOnTime(){
int handledStatus=2;
Specification<Ticket> allTicketHandledOnTimeQuery =where(TicketSpecification.isHandled(handledStatus)).and(TicketSpecification.isOntime(purposes));
return String.valueOf(ticketRepository.count(allTicketHandledOntimeQuery));
}
}
Here is Purpose POJO Model:
public class Purpose{
private String id;
private String name;
private String description;
private int level;
private int duration;
}
Here is PurposeUtils :
It takes a list of purposes and generate a hashmap of purpose and number of days it should take to handle a ticket of that purpose.
public class PurposeUtils {
public static Map<String, Integer> PurposeArrayToNameDurationMap(ArrayList<Purpose> purposes) {
Map<String, Integer> purposeMap = new HashMap<String, Integer>();
for(Purpose purpose: purposes) {
purposeMap.put(purpose.getName(), purpose.getDuration());
}
return purposeMap;
}
}
I assume you use javax.persistence.criteria.Root in this line:
nameAndDurationMap.get(root.get(Ticket_.purpose)) * 86400);/*Line 208*/
note documentation of Root:
Path< Y > get(String attributeName) Create a path corresponding to the
referenced attribute.
so you ask the map to get the value that indeed is not there
Note that root is not a value holder, it is a for the prdicat creation, so in your predict you will say I want value X(root) to met Y condition
this will become SQL query, so it has to be values that SQL can handle, your code will not be called on every ticket... if you want to do it either makes it iterate every purpose
(
if purpose_x == Ticket_.purpose and le(...)
or purpose_y == Ticket_.purpose and le(...)
)
or move the logic to DB function you can call
code that will give the idea but probably will not run since it dry:
public class TicketSpecification {
public static Specification<Ticket> isOntime(ArrayList<Purpose> purposes){
return (root, query, builder) -> {
List<Predicate> predicates = new ArrayList<>();
for(Purpose purpose: purposes) {
predicates.add(isOntime(purpose.getName(), purpose.getDuration(),root, query, builder);
}
return builder.or(predicates.toArray(new Predicate[0]));
}
}
public static Predicate isOntime(String purposes_name,int purposes_time,Root<Ticket> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
return builder.and(
builder.equal(root.get(Ticket_.purpose),purposes_name)
,
builder.le(
builder.diff(
builder.function("unix_timestamp", Long.class, root.get(Ticket_.handledAt)),
builder.function("unix_timestamp", Long.class, root.get(Ticket_.createdAt))
)
,(purposes_time * 86400);/*Line 208*/
)
);
}
}

Rewrite code to get only several variables

I have this code which I would like to use to translate keys and return data to front end:
#GetMapping("pages")
public Page<ContractDTO> pagxes(#RequestParam(value = "page") int page, #RequestParam(value = "size") int size) {
return contractService.findAll(page, size)
//.map(mapper::toDTO);
.map(g -> new ContractDTO(g.getName(), getMerchantName(g.getMerchant_id())));
}
private String getMerchantName(int id) {
Optional<Merchants> obj = merchantService.findById(id);
return obj.get().getName();
}
DTO :
public class ContractDTO {
private Integer id;
.....
private Integer acquirer_id;
private Integer terminal_id;
private String merchant_id;
......
}
How I can rewrite this code .map(g -> new ContractDTO(g.getName(), getMerchantName(g.getMerchant_id()))); to translate from int to String using getMerchantName(int id) only terminal_id and merchant_id and all other variables not to be translated?
I can create constructor in ContractDTO but the code will be huge. Is there some other way?
Error:
The method builder() is undefined for the type ContractDTO
In your case because you want to avoid multiple constructors, You can use a builder design pattern, by using lombok library, it can be more easier, so you can just annotate your class of ContractDTO with this library annotation, and you have every thing to go :
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#Builder(toBuilder = true)
class ContractDTO {
private Integer id;
private String name;
private Integer acquirerId;
private Integer terminalId;
private String merchantId;
}
then your code can be :
...
.map(g -> ContractDTO.builder()
.name(g.getName())
.merchantName(g.getMerchantId())
.build()
)....

Dynamically Query from List or two different entities

I have an entity called Person, inside that basic metadata, then inside that Tag and Language. I want to get all rows that contain specific tag name and language.
I came to know about Criteria Query about. How can we interlink two different entities together?
Example: Get all rows having the tag as Model and language as English.
#Entity
public Person {
#Id
private String id;
private BasicMetadata basicMetadata;
-----------
}
Basic Metadata table
#Entity
public BasicMetadata {
#Id
private String id;
private List<Tag> tags;
private List<Language> language;
-------------
}
Tag Table
#Entity
public Tag {
#Id
private String id;
private String name;
-------------
}
Language Table
#Entity
public Language{
#Id
private String id;
private String name;
-------------
}
I created a simple method for specification Query is that correct
private Specification<Person> containsText(String keyword) {
return (root,query, builder) -> {
String finalText = keyword.toLowerCase();
if (!finalText.contains("%")) {
finalText = "%" + finalText + "%";
}
Predicate genreExp = builder.like(builder.lower(root.get("basicMetadata").get("tags")), finalText);
return builder.or(genreExp);
};
you can write your specification like this
public class PersonSpecifications {
public static Specification<Person> hasTag(String keyword) {
return (root, query, builder) -> {
String finalText = keyword.toLowerCase();
if (!finalText.contains("%")) {
finalText = "%" + finalText + "%";
}
Join<Person, BasicMetaData> md = root.join("basicMetaData");
return builder.like(builder.lower(md.join("tags").get("name")), finalText);
}
}
}
and you can use this specification to get the filtered results like this
repository.findAll(PersonSpecifications. hasTag("abc"),PageRequest,of(0,10));

Mapping flat DB documents to nested classes in Morphia

I am porting a Java application that used to use Jongo to communicate with MongoDB to Morphia + new MongoDB Java Driver.
In the database there are flat documents like:
{
"_id" : ObjectId("56c2e5b9b9b6e9753d4f11de"),
"field1" : "foo",
"field2" : "bar",
"field3" : NumberLong("1455613369030"),
"field4" : NumberLong("1455613369030"),
"field5" : NumberLong(58),
"field6" : NumberLong(1)
}
And there are not flat entity classes to store those documents in Java app, annotated using jackson to be usable with Jongo:
public class MyPOJOClass {
public static final String COLLECTION_NAME = "foobar";
#JsonProperty(value = "_id")
private String id;
#JsonUnwrapped
private FooBar foobar;
#JsonUnwrapped
private MyLongs longs;
// Constructor and other methods
public static class FooBar {
#JsonProperty(value = "field1")
private String foo;
#JsonProperty(value = "field2")
private String bar;
// Consteructor and other methods…
}
public static class MyLongs {
#JsonProperty(value = "field3")
private long first;
#JsonProperty(value = "field4")
private long second;
// etc…
}
}
Can I somehow port this exact structure to Morphia as is, without flattening the entity classes or expanding documents (so that foo and bar fields are in one embedded document and LongNumber fields in another document)?
I have not found any examples of the #Embedded annotation (the only one that looks relevant and gives some hope) to do such a thing. I want to end up with something like:
#Entity(MyPOJOClass.COLLECTION_NAME)
public class MyPOJOClass {
public static final String COLLECTION_NAME = "foobar";
#Id
private ObjectId id;
#Embedded(_SOME_MAGIC_?)
private FooBar foobar;
#Embedded(_SOME_MAGIC_?)
private MyLongs longs;
// Constructor and other methods
#Embedded(_SOME_MAGIC_?)
public static class FooBar {
#Property("field1")
private String foo;
#Property("field2")
private String bar;
// Consteructor and other methods…
}
#Embedded(_SOME_MAGIC_?)
public static class MyLongs {
#Property("field3")
private long first;
#Property("field4")
private long second;
// etc…
}
}

multiple language support data from db

My application has entities with nameEn and nameDe for english and german. But only english being used now. Since there are so many entities available, I wanted to have a generic class which can return the selected language entries,but for multiple entries my approach didn't work.
#Entity
#Table(name="employee")
public class Employee implements java.io.Serializable {
// Fields
private Integer id;
private String nameEn;
private String nameDe;
//Getter, Setter Methods
}
#Entity
#Table(name="address")
public class Address implements
java.io.Serializable {
private String descriptionEn;
private String descriptionDe;
}
public interface ILabelText {
String getNameEn();
String getNameDe();
String getDescriptionEn();
String getDescriptionDe();
}
public abstract class LabelText implements ILabelText, Serializable {
private static final long serialVersionUID = 1L;
protected String descriptionEn;
protected String descriptionDe;
private Logger log = Logger.getLogger(LabelText.class);
String language = FacesContext.getCurrentInstance().getViewRoot().getLocale().getLanguage();
public String getDescription() {
log.info("Language Selected is " + language);
if (language.equals("De")) {
return getDescriptionDe();
} else {
return getDescriptionEn();
}
}
public String getName() {
log.info("Language Selected is " + language);
if (language.equals("De")) {
return getNameDe();
} else {
return getNameEn();
}
}
}
//In Xhtml, based on selected locale, display value accordingly
<h:outputText value="#{emp.getName()}" />
<h:outputText value="#{add.getDescription()}" />
You can create an entity Lang like this
#Entity
public class Lang implements Serializable
{
#Id #GeneratedValue(strategy=GenerationType.IDENTITY)
private long id;
#NotNull
private String key;
#NotNull
private String translation;
}
and use it in your Address like this
#OneToMany(cascade = CascadeType.ALL, fetch=FetchType.EAGER)
#MapKey(name = "key")
protected Map<String, Lang> name;
Then you are able to access the correct language in JSF:
<h:outputText value="#{emp.name[userLocale].translation}" />
The expression userLocale should be resolved to your language key (en, de, ...) or can be hardcoded e.g. #{emp.name['en'].translation}.
Is more easy you create a table with translations:
e.g:
People -> All of your persons
PersonTranslations
People | id
PersonTranslations | locale; person_id;
then on your Person class you set the language for all attributes on predicate
Person.description (this will search on PersonTranslation using a person_id key, and a locale)
some like that PersonTranslation.find(1, 'en');

Categories

Resources