===== POJO =====
// Employee POJO
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonNaming(PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy.class)
public class Employee implements Serializable {
private Integer id;
private String name;
private Integer companyId;
// assume getters ,setters and serializable implementations.
}
// Company POJO
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonNaming(PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy.class)
public class Company implements Serializable {
private Integer id;
private String name;
// assume getters ,setters and serializable implementations.
}
// EmployeeVO POJO
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonNaming(PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy.class)
public class EmployeeVO implements Serializable {
private Employee employee;
private Company company;
// assume getters ,setters and serializable implementations.
}
===== My DAO layer class =====
public List<EmployeeVO> getEmployees(){
// configuring model mapper.
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration()
.addValueReader(new RecordValueReader())
.setSourceNameTokenizer(NameTokenizers.UNDERSCORE);
//property map configuration.
PropertyMap<Record, EmployeeVO> employeeVOMap = new PropertyMap<Record, EmployeeVO>() {
protected void configure() {
map().getEmployee().setName(this.<String>source("name"));
map().getEmployee()..setId(this.<Integer>source("id"));
map().getCompany().setName(this.<String>source("comp_name"));
map().getCompany().setId(this.<String>source("comp_id"));
}
};
// TypeMap config
modelMapper.createTypeMap(Record.class, EmployeeVO.class);
// adding employeeVOMap .
modelMapper.addMappings(employeeVOMap);
// JOOQ query
List<Field<?>> fields = Lists.newArrayList();
// fields includes, id, name, comp_name, comp_id
SelectJoinStep query = select(dslContext, fields).from(EMPLOYEE)
.join(COMPANY)
.on(COMPANY.ID.equal(EMPLOYEE.COMPANY_ID));
Result<Record> records = query.fetch();
Record record = null;
Iterator<Record> it = records.iterator();
List<EmployeeVO> employeeList= Lists.newArrayList();
while (it.hasNext()) {
record = it.next();
EmployeeVO employeeVOObj =
modelMapper.map(record, EmployeeVO.class);
employeeList.add(employeeVOObj);
}
return employeeList;
}
===== Error log =====
1) Error mapping org.jooq.impl.RecordImpl to com.myportal.bingo.db.model.EmployeeVO
1 error] with root cause
java.lang.ArrayIndexOutOfBoundsException: -1
Note:
ModelMapper throws the above exception when it reaches below method.
private void matchSource(TypeInfo<?> sourceTypeInfo, Mutator destinationMutator)
in ImplicitMappingBuilder.java
sourceTypeInfo.getAccessors() is null.
Any help?
Had the same problem, or at least which looked the same. (You can move directly to my solution in the last paragraph.) Lots of debugging have shown the following:
if accessors on that line (mentioned in your question) are null, then accessors = PropertyInfoSetResolver.resolveAccessors(source, type, configuration) line in TypeInfoImpl class is executed, and the reason of exception in my case was this call:
valueReader.get(source, memberName) at the following piece of code at 'resolveAccessors' method in the PropertyInfoSetResolver class:
if (valueReader == null)
resolveProperties(type, true, configuration, accessors);
else {
NameTransformer nameTransformer = configuration.getSourceNameTransformer();
for (String memberName : valueReader.memberNames(source))
accessors.put(nameTransformer.transform(memberName, NameableType.GENERIC),
new ValueReaderPropertyInfo(valueReader, valueReader.get(source, memberName),
memberName));
which ends up in source.getValue(memberName.toUpperCase()), where source is JOOQ's Record; InvoiceRecord in my case. And - tada - for some reason invoice.getValue("INVOICE_ID") ends up in the exception (no such field and therefore indexOf returns -1 which causes the ArrayIndexOutOfBoundsException), while invoice.getValue("invoice_id") is totally fine.
So else condition (the same piece of code above) wasn't the right way to execute the code, and if case turned out to be ok.
So that's what helped me in my particular case: removing of the row modelMapper.getConfiguration().addValueReader(new RecordValueReader()). Hope this will help you too.
Related
I'm not sure how to phrase the question title to be honest, if someone has a suggestion, please let me know.
My use case is this, I have an entity with an account property like so (this is cleaned up to avoid clutter):
#Entity
#Table(name = "report_line", schema = "public")
public class ReportLine extends BaseReportLine {
#ManyToOne
#JoinColumn(name = "report_id")
private Report report;
#ManyToOne
#JoinColumn(name = "account_id")
private Account account;
}
But a DTO that only has an account id / different properties:
public class ImportLineDto {
public String groupName;
public Integer position;
public Integer parentPosition;
public String accountId;
public String name;
public BigDecimal amount;
public List<ImportLineDto> lines = new ArrayList<>();
}
I need to go through / flatten all lines so I can save it to a JPA repository, but there are 2 issues:
Is there a way to create the table line object using the accountId only, without having to look up the account for each line, as that will add a massive amount of unnecessary db calls.
What should I do with the 'lines' on each table object after flattening? Should I set them to null / empty list?
Is there a better way to do this? For once I can actually make changes to the code
Here is what I have so far:
private void saveReport(ImportedResult result) {
Report report = new Report();
...
report.setLines(getReportLinesFromDtoLines(result.lineItems.lines));
ReportRepository.saveAndFlush(report);
}
private List<ReportLine> getReportLinesFromDtoLines(ImportLineDto lines) {
List<ImportLineDto> flatLines = flatMapRecursive(lines).collect(Collectors.toList());
List<ReportLine> reportLines = new ArrayList<>();
for(ImportLineDto line: flatLines) {
ReportLine reportLine = new ReportLine();
reportLine.setItemText(line.name);
reportLine.setAmount(line.amount);
reportLine.setAccount(???);
// how do I set the 'Account' property using the id only, without looking up each account?
reportLines.add(reportLine);
}
return ReportLines;
}
public Stream<ImportLineDto> flatMapRecursive(ImportLineDto item) {
if (item.lines == null) {
return Stream.empty();
}
return Stream.concat(Stream.of(item), item.lines.stream()
.flatMap(this::flatMapRecursive));
}
Follow up:
Just to throw a wrench in there, what if the DTO accountId was not the actual "id" field in the table, but another custom field, I have another situation like that, would it even be possible? I still need the answer the the 1st question however with a standard id.
you may use entityManager.getReference as explained here
reportLine.setAccount(entityManager.getReference(Account.class, line.accountId));
I have the following Entities
public class Manufacturer
{
int id;
String name;
Country country;
List<Model> models;
}
public class Model
{
int id;
String name;
}
And the following DTO
public class ManufacturerLastModelDto
{
Integer id;
String name;
ModelDto model;
}
public class ModelDto
{
int id;
String name;
}
Now I want to map the Manufacturer to the ManufacturerLastModelDto, like that:
modelMapper.map(manufacturer, ManufacturerLastModelDto.class)
So that only the first entry of the List model will be assigned from manufacturer.
My previous solution was that I had a List of ModelDto's even in the DTO and removed all Entries after Index 0. That was OK, because the ModelMapper mapped the child from Model to ModelDTO automatically.
But only Response wasn't so nice:
models: [
{...}
]
because it was sent as an Array.
Do I need a custom ModelMapper here? If so, how to build it? The tutorial is really complex. Do I need a converter or a TypeMap (or both)?
I am not too familiar with ModelMapper but have used it occasionally.
Yes, you would need to create a converter for the property, and you could either use it with the ModelMapper or a TypeMap. Only caveat being that you will need to map the property yourself, for instance
The converter
Converter<List<Model>, ModelDto> modelConverter = new AbstractConverter<List<Model>, ModelDto>() {
#Override
protected ModelDto convert(List<Model> models) {
if (models == null || models.isEmpty()) {
return null;
}
Model model = models.get(0);
ModelDto dto = new ModelDto();
dto.setId(model.getId());
dto.setName(model.getName());
return dto;
}
};
Now using default model mapper
ModelMapper modelMapper = new ModelMapper();
modelMapper.addConverter(modelConverter);
ManufacturerLastModelDto result = modelMapper.map(manufacturer, ManufacturerLastModelDto.class);
Or using the TypeMap
TypeMap<Manufacturer, ManufacturerLastModelDto> typeMap = modelMapper.typeMap(Manufacturer.class, ManufacturerLastModelDto.class)
.addMappings(mapper ->
mapper.using(modelConverter).map(Manufacturer::getModels, ManufacturerLastModelDto::setModel)
);
ManufacturerLastModelDto result = typeMap.map(manufacturer);
I have this POJO :
public class PlayerDto {
private Long id;
private String name;
private String past;
}
And I have this entity :
public class Player {
private Long id;
private String name;
private List<String> past;
}
How can I map the List<String> past into the String past of the DTO wih MapStruct ? For example the List is containing [ Monty , Boto , Flaouri ] and the String of the DTO has to contain "Monty, Boto, Flaouri" in a single String.
This classic way doesn't work with the target and source :
#Mappings({
#Mapping(target = "past", source = "past"),
})
PlayerDto entityToDto(final Player entity);
Thanks
I guess you need to define a default method in your mapper interface to handle data conversion from List<String> to String. Mapstruct will automatically use the default method.
The default method signature for your mapping should be like this :
String map(List<String> past)
Example :
default String map(List<String> past) {
return past.stream().collect(Collectors.joining(","));
}
I have these two classes:
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id",scope = Rol.class)
public class Rol extends MyEntity implements Serializable {
private Integer id;
private String rolName;
public Rol(Integer id, String rolName) {
this.id = id;
this.rolName = rolName;
}
...
}
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id",scope = User.class)
public class User extends MyEntity implements Serializable {
private Integer id;
private String name;
private List<Rol> rolList;
public User(Integer id, String name, List<Rol> rolList) {
this.id = id;
this.name = name;
this.rolList = rolList;
}
...
}
and I try to serialize and deserialize the user object as following
Rol rol1 = new Rol(1, "MyRol");
Rol rol2 = new Rol(1, "MyRol");
List<Rol> rolList = new ArrayList();
rolList.add(rol1);
rolList.add(rol2);
user = new User(1, "MyUser", rolList);
ObjectMapper mapper = new ObjectMapper();
String jsonString = mapper.writeValueAsString(user);
User userJson = mappe.readValue(jsonString, User.class);
and the JsonMappingException: Already had POJO for id is produced. Why?
When I review the json result of the serialization I see that the result is
{"id": 1,"name": "MyName","rolList": [{"id": 1,"rolName": "MyRol"},{"id": 1,"rolName": "MyRol"}]}
when the result should be
{"id": 1,"name": "MyName","rolList": [{"id": 1,"rolName": "MyRol"},1]}
because rol1 and rol2 are different instances of the same POJO identifier with id 1.
How can I avoid the JsonMappingException? In my project I have some different instances of the same POJO. I can guarantee that if the id's are equal -> objects are equal.
Excuse me for my bad English.
For anyone returning to this question, it looks like there's option to do this with a custom ObjectIdResolver in Jackson. You can specify this on the #JsonIdentityInfo annotation, e.g. :
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "name",
resolver = CustomObjectIdResolver.class)
Then perhaps wrap the normal SimpleObjectIdResolver class to get going and customise bindItem().
In my case I wanted to avoid overlapping objectIds, so cleared down the references when I started a new Something:
public class CustomObjectIdResolver implements ObjectIdResolver {
private ObjectIdResolver objectIdResolver;
public CustomObjectIdResolver() {
clearReferences();
}
#Override
public void bindItem(IdKey id, Object pojo) {
// Time to drop the references?
if (pojo instanceof Something)
clearReferences();
objectIdResolver.bindItem(id, pojo);
}
#Override
public Object resolveId(IdKey id) {
return objectIdResolver.resolveId(id);
}
#Override
public boolean canUseFor(ObjectIdResolver resolverType) {
return resolverType.getClass() == getClass();
}
#Override
public ObjectIdResolver newForDeserialization(Object context) {
return new CustomObjectIdResolver();
}
private void clearReferences() {
objectIdResolver = new SimpleObjectIdResolver();
}
}
Jackson expects in this case different id for different class instances. There has been a previous discussion at github here. Overriding hashCode and equals will not help. Object references must match for equal id.
Options
Reuse Rol instances instead of making new ones with equal fields. As a bonus you will also save memory.
Modify the application logic so that it doesn't depend on #JsonIdentityInfo
I'm not being able to query a MongoDB document according to field values of an embedded Java Collection.
I have the following entity:
#Entity
public class StationHistoryEntry // extends ...
{
#Embedded
private Set<SongFeedback> songFeedback = new HashSet<SongFeedback>();
// ...
}
And the following embedded class:
#Embedded
public class SongFeedback // extends ...
{
#Embedded
private FeedbackType feedbackType;
private ObjectId userId;
public enum FeedbackType {
THUMBS_UP, THUMBS_DOWN, STAR;
}
// other properties
}
What I need to do is to find StationHistoryEntries that have SongFeedback with a given userId and feedbackType=STAR.
I've tried the following but didn't succeed when the other SongFeedback properties (the ones not shown on the code snippet because I don't have control over their values) were not null, which happens in production:
public List<StationHistoryEntry> findStarredByUserId(ObjectId userId) {
SongFeedback songFeedback = new SongFeedback(FeedbackType.STAR, userId);
return ds.find(StationHistoryEntry.class)
.filter("songFeedback elem", songFeedback).asList();
}
And I've also tried the following, but it always returns an empty list:
public List<StationHistoryEntry> findStarredByUserId(ObjectId userId) {
Query<StationHistoryEntry> query = ds.createQuery(StationHistoryEntry.class);
query.and(
query.criteria("songFeedback.userId").equal(userId),
query.criteria("songFeedback.feedbackType").equal(FeedbackType.STAR));
return query.asList();
}
If it helps at all, I've created a Github repository with the stripped down code and a unit test: https://github.com/gabrielcs/MorphiaQueryStackOverflow
Any ideas? Thanks!
try this
public List<StationHistoryEntry> findStarredByUserId(ObjectId userId) {
Query<StationHistoryEntry> query = ds.createQuery(StationHistoryEntry.class);
query.and(
query.criteria("songFeedback.userId").equal(userId),
query.criteria("songFeedback.feedbackType").in(FeedbackType.STAR));
return query.asList();
}