Mapstruct does not use builders defined by Lombok - maven

Solution:
I had to change the ordering of my mapstruct and lombok annotationProcessorPaths.
I had to place mapstruct above lombok, then it worked.
I updated the pom below to the working version, so there is no non-working-code in here.
I also converted the lombok version back to the current release and not using the edge-version.
Original Problem:
I have 2 more or less identical sets of classes (see example below)
one set are the DTOs of my API, which I want to have immutable, using Lombok's #Value and #Builder
one set are the entities that are going to be stored in the database. With Lombok's #Data
Initially I set the project up to use:
Lombok 1.18.12
Mapstruct 1.3.1
Java 11
Maven
I found the Lombok documentation explaining how to add the annotation-processor to the maven-plugin
https://projectlombok.org/setup/maven
But when executing I still get Error:(16,25) java: ClassX does not have an accessible parameterless constructor.
Searching for this message I found some 2 to 3 years of problems, but nothing up to date. Also I saw, that the issue was resolved for those posts.
In at least one of the posts it was mentioned, that it worked, when splitting the project into modules. And this worked for me as well. When I move the DTOs to another maven module, build them there and set the dependency it works, but this is definitely not the project-structure I want to have. Also since I might need to move my entities out as well and I don't want to create a new module for each Pojo-structure I'm creating.
I also found that post on the Lombok Edge version:
https://projectlombok.org/download-edge
The second point in the change-list is
BREAKING CHANGE: mapstruct users should now add a dependency to lombok-mapstruct-binding. This solves compiling modules with lombok (and mapstruct).
So I tried that as well.
I added the repository to my pom, added lombok-mapstruct-binding and set the lombok version to edge-SNAPSHOT
But even after a clean the compile step fails.
In between I changed my DTOs to use #Data as well, but I would like to change this back.
Finally here are some examples and details on the code.
DTOs
#Data
#AllArgsConstructor(access = AccessLevel.PROTECTED)
#NoArgsConstructor(access = AccessLevel.PROTECTED)
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = BDto.class, name = "b"),
#JsonSubTypes.Type(value = CDto.class, name = "c")
})
public abstract class ADto {
private long id;
private String type;
private Set<String> metadata;
private Set<String> tags;
}
#Data
#NoArgsConstructor(access = AccessLevel.PRIVATE)
public class BDto extends ADto {
private String path;
#Builder
private BDto(long id, String path, Set<String> metadata, Set<String> tags) {
super(id, "b", metadata, tags);
this.path = path;
}
}
#Data
#NoArgsConstructor(access = AccessLevel.PRIVATE)
public class CDto extends ADto {
private String name;
private Set<A> collection;
#Builder
private CDto(long id, String name, Set<A> collection, Set<String> metadata, Set<String> tags) {
super(id, "c", metadata, tags);
this.collection = collection;
this.name = name;
}
}
Entities
#Entity
#Table
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "type")
#AllArgsConstructor
#NoArgsConstructor
#Getter
public abstract class A extends PanacheEntityBase {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
protected long id;
#Column(name = "type", insertable = false, updatable = false)
private String type;
/* ... */
}
#Entity
#DiscriminatorValue("b")
#NoArgsConstructor
#Getter
#ToString
public class B extends A {
public B(long id, String path, Set<String> metadata, Set<Tag> tags) {
super(id, "b", metadata, tags);
this.path = path;
}
public B(String path) {
super(0, "b", new HashSet<>(), new HashSet<>());
this.path = path;
}
#Column(name = "path")
#Setter
private String path;
}
#Entity
#DiscriminatorValue("c")
#NoArgsConstructor
#Getter
public class C extends A {
public C(long id, String name, List<A> collection, Set<String> metadata, Set<Tag> tags) {
super(id, "c", metadata, tags);
this.collection = collection;
this.name = name;
}
#Column(name = "name")
private String name;
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name = "c_id")
#OrderBy("order")
List<A> collection;
}
Mappers
public interface AMapper {
default String tagToDto(Tag tag) {
return tag.getTag();
}
default Tag tagFromDto(String tag) {
return Tag.createIfNotExists(tag);
}
}
#Mapper()
public interface BMapper extends AMapper {
#Override
#Mapping(target = "tags",
qualifiedByName = "tagToDto")
BDto toDto(B b);
#Override
#Mapping(target = "tags",
qualifiedByName = "tagToEntity")
B toEntity(BDto b);
}
#Mapper()
public interface CMapper extends AMapper {
#Override
#Mapping(target = "tags",
qualifiedByName = "tagToDto")
CDto toDto(C b);
#Override
#Mapping(target = "tags",
qualifiedByName = "tagToEntity")
C toEntity(CDto b);
}
Pom
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<artifactId>dummy</artifactId>
<groupId>dummy</groupId>
<version>0.1.0</version>
<packaging>pom</packaging>
<properties>
<compiler-plugin.version>3.8.1</compiler-plugin.version>
<maven.compiler.parameters>true</maven.compiler.parameters>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<lombok.version>1.18.12</lombok.version>
<mapstruct.version>1.3.1.Final</mapstruct.version>
</properties>
<repositories>
<repository>
<id>projectlombok.org</id>
<url>https://projectlombok.org/edge-releases</url>
</repository>
</repositories>
<dependencies>
<!-- other stuff -->
<!-- Tools -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<!-- <scope>provided</scope> -->
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</annotationProcessorPath>
<annotationProcessorPath>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>

With lombok (1.18.18) and mapstruct (1.4.2.Final) everything worked after I:
added plugin lombok-mapstruct-binding
added lombok-mapstruct-binding to annotationProcessorPaths section of plugin maven-compiler-plugin
links:
github example pom.xml: https://github.com/mapstruct/mapstruct-examples/blob/master/mapstruct-lombok/pom.xml
from https://mapstruct.org/faq/ :
If you are using Lombok 1.18.16 or newer you also need to add lombok-mapstruct-binding in order to make Lombok and MapStruct work together.

Related

Quarkus Reactive with Vert.x and Hibernate Reactive / java.lang.NullPointerException: Cannot store to object array because "this.loadedState" is null

i am trying to use quarkus reactive with vert.x and hibernate reactive.
this is my pom.xml:
<quarkus-plugin.version>1.12.2.Final</quarkus-plugin.version>
and
<quarkus.platform.version>1.12.2.Final</quarkus.platform.version>
with:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-reactive-mysql-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx-web</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-reactive</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-reactive-pg-client</artifactId>
</dependency>
this is my application.properties file:
# postgres-configuration
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=partner_usr
quarkus.datasource.password=postgrespw
quarkus.datasource.reactive.url=vertx-reactive:postgres://localhost:3310/partnerdb
# test, but not working (schema's won't created)
quarkus.hibernate-orm.database.generation.create-schemas=true
# working (drop-and-create only on mysql, not on postgres)
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
quarkus.http.cors=true
Then, i have following entities:
#Data
#MappedSuperclass
public abstract class IdEntity {
#Id
#SequenceGenerator(name = "entitySeq", sequenceName = "entitiy_id", allocationSize = 1, initialValue = 5)
#GeneratedValue(generator = "entitySeq", strategy = GenerationType.AUTO)
private Long id;
}
#Data
#Entity
#EqualsAndHashCode(callSuper = true)
public class Person extends IdEntity {
private String firstName;
private String lastName;
public Person() {
}
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Address personAddress;
}
#Data
#Entity
#EqualsAndHashCode(callSuper = true)
public class Address extends IdEntity {
private String street;
private String houseNumber;
private int postalCode;
private String city;
#OneToMany(orphanRemoval = true, mappedBy = "personAddress", fetch = FetchType.LAZY)
private List<Person> persons = new ArrayList<>();
public Address() {
}
}
Now, i am calling a reactive web-service with a reactive db access:
#Path("/person")
#ApplicationScoped
public class PersonResource {
#Inject
io.vertx.mutiny.pgclient.PgPool sqlClient;
#Inject
Mutiny.Session mutinySession;
#GET
//#Produces(MediaType.APPLICATION_JSON)
#Path("/list-persons")
#Route(path = "/list-persons", methods = HttpMethod.GET, produces = MediaType.APPLICATION_JSON)
#Transactional
public Multi<Person> listAllPersons() {
// return sqlClient.query("SELECT * FROM Person ORDER BY lastName ASC").execute()
// .onItem().transformToMulti(set -> Multi.createFrom().iterable(set))
// .onItem().transform(this::transformPersons);
return mutinySession.createQuery("SELECT f FROM Person f ORDER BY f.lastName")
.getResults().onItem().transform(this::transformObject);
}
private Person transformObject(Object f) {
return (Person)f;
}
private List<Object> transformPersons(Object f) {
final Person person = (PartnerMockEntity)f;
final List<Object> bogus = new ArrayList<>();
bogus.add(partner);
return bogus;
}
}
Exception:
Resulted in: com.fasterxml.jackson.databind.JsonMappingException: Cannot store to object array because "this.loadedState" is null (through reference chain: de.subito.model.Person["personAddress"]->de.subito.model.Address["person"])
I tried to use :
FetchType.EAGER on Address in Person
I removed the #OneToMany Relation in Address: this solves the error (yay), but the addresses won't be returned in the resulting json (id is existing, but the values are not fetched)
The questions is, how can i fetch in reactive those kind of relations without getting errors?
Or do i need a angular page in order to display this correctly?
Somehow i forgot about how fetchType.Lazy works.
Simply add a join fetch into the hql and everything works as expected.
SELECT p from Person p left join fetch p.personAddress
When using this query, there's no session/closed or any other exception thrown and the json result will be displayed as expected.
Additional note: in order to avoid recursive serialization, it is required to use the
#JsonManagedReference and #JsonBackReference
Annotations, depending on your needs to your relations.

Mapstruct Implementation

I am using mapstruct to map my model to my DTO.
I want to search for a record by the full name.
I do not understand why I get the following errors:
Error creating bean with name 'customerController'
Error creating bean with name 'customerServiceImpl'
Error creating bean with name 'customerRepository'
No property name found for type Customer!
this is my project
public interface CustomerMapper {
CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class);
#Mapping(source = "lastName", target = "lastName")
CustomerDTO customerToCustomerDTO(Customer customer);
}
#Data
public class CustomerDTO {
private String firstName;
private String lastName;
}
#Data
#Entity
#Getter
#Setter
public class Customer {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String name;
}
#Data
#NoArgsConstructor
#AllArgsConstructor
public class CustomerListDTO {
List<CustomerDTO> categories;
}
#Controller
#RequestMapping("api/v1/customers")
public class CustomerController {
private final CustomerService customerService;
public CustomerController(CustomerService customerService) {
this.customerService = customerService;
}
#GetMapping("{name}")
public ResponseEntity<CustomerDTO> getCustomerByName(#PathVariable String name) {
return new ResponseEntity<>(
customerService.getCustomerByName(name), HttpStatus.OK
);
}
public interface CustomerRepository extends JpaRepository<Customer, Long> {
Customer findByName(String x);
}
public interface CustomerService {
CustomerDTO getCustomerByName(String name);
}
#AllArgsConstructor
#Service
public class CustomerServiceImpl implements CustomerService {
CustomerMapper customerMapper;
CustomerRepository customerRepository;
#Override
public CustomerDTO getCustomerByName(String lastName) {
return customerMapper.customerToCustomerDTO(customerRepository.findByName(lastName));
}
}
This is a potential fix: would be to map the below in the CustomerMapper, but to me it doesn't feel right.
#Mapping(source = "name", target = "lastName")
#Mapping(source = "firstName", target = "firstName")
In the documentation, it is said that you can map whatever field from model to DTO, I think there might be something wrong in my code.
The way I try implementing in the repo, service, controller.
Edit:
Maybe a solution would be to use DTO in Repository?
Update:
#Override
public CustomerDTO getCustomerByName(String lastName) {
return customerRepository.findByName(lastName).map(customerMapper::customerToCustomerDTO);
}
.map cannot be used.
for .map to be used I should use code like this
.findAll()
.stream()
.map(customerMapper::customerToCustomerDTO)
.collect(Collectors.toList());
I am using the findByName method however, that doesn't have access to .map.
How can I solve that problem?
EDIT
this is how my Customer I think should look like
#Data
#NoArgsConstructor
#AllArgsConstructor
public class CustomerDTO {
private String id;
private String firstName;
private String lastName;
}
"No property name found for type Customer!"
In you table costumer you have a column with name "name"?
Below I made some changes in your code, however if you need to find by name your repository needs to find correct search. When you use findByName only return rows where name is equals to name passed in parameter. Example: findByName("Scilla") only return rows where column name is equals to "Scilla", if a column name have values like "scilla" (lower) or "Scilla abc" this entries was not returned by query.
Method findByName with value "Scilla" generate this query:
select * from customer where name = 'Scilla';
Code changes
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CustomerRepository extends JpaRepository<Customer, Long> {
Customer findByLastName(String lastName);
List<Customer> findByLastNameContainingIgnoreCase(String name);
List<Customer> findByLastNameContaining(String name);
}
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
#Mapper
public interface CustomerMapper {
CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class);
CustomerDTO customerToCustomerDTO(Customer customer);
Customer toDomain(CustomerDTO customerDTO);
}
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import lombok.Data;
import lombok.NoArgsConstructor;
#Data
#Entity
#NoArgsConstructor
public class Customer {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
}
import lombok.Data;
#Data
public class CustomerDTO {
private String firstName;
private String lastName;
}
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
#RequestMapping("api/v1/customers")
public class CustomerController {
private final CustomerService customerService;
public CustomerController(CustomerService customerService) {
this.customerService = customerService;
}
#GetMapping("{name}")
public ResponseEntity<CustomerDTO> getCustomerByName(#PathVariable String name) {
return new ResponseEntity<>(
customerService.getCustomerByName(name), HttpStatus.OK
);
}
#PostMapping
public ResponseEntity<CustomerDTO> getCustomerByName(#RequestBody CustomerDTO customerDTO ) {
return new ResponseEntity<>(
customerService.save(customerDTO), HttpStatus.OK
);
}
}
Important
Below put Spring Data query and translation query.
List<Customer> findByLastNameContainingIgnoreCase(String name)
select * from customer where last_name ilike = '%name%';
pom.xml definition
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>11</java.version>
<org.mapstruct.version>1.4.1.Final</org.mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.1.Final</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<forceJavacCompilerUse>true</forceJavacCompilerUse>
<release>11</release>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
To map your Customer entity to DTO and use it in the Spring application you should use the following mapper (with parameter componentModel = "spring"):
#Mapper(
componentModel = "spring",
nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE
)
public interface CustomerMapper {
#Mapping(target = "lastName", source = "name")
CustomerDto toDto(Customer customer);
}
MapStruct doesn't know how to map name property in your entity to lastName property in the DTO, so you have to specify this in #Mapping annotation.
(I also recommend to use specified values of 'strategy' parameters - you can check their purpose in javadoc.)
In this case, MapStruct generates an appropriate Spring bean with the implementation of your mapper, something like this:
#Component
public class CustomerMapperImpl {
public CustomerDto toDto(Customer customer) {
CustomerDto dto = new CustomerDto();
if (customer != null) {
if (customer.getFirstName() != null) {
dto.setFirstName(customer.getFirstName());
}
if (customer.getName() != null) {
dto.setLastName(customer.getName());
}
}
return dto;
}
}
So Spring will be able to inject that bean in your service (don't forget to correct findByName method of your repo to return Optional):
#RequiredArgsConstructor
#Service
public class CustomerServiceImpl implement CustomerService {
private final CustomerRepo repo;
private final CustomerMapper mapper;
#Override
public Optional<CustomerDto> getByName(#NonNull String name) {
return repo.findByName(name).map(mapper::toDto)
}
#Override
public List<CustomerDto> getAll() {
return repo.findAll().stream().map(mapper::toDto).collect(Collectors.toList());
}
}
And then use this service in your REST controller:
#RequiredArgsConstructor
#RestController
#RequestMapping("api/v1/customers")
public class CustomerController {
private final CustomerService service;
#GetMapping("/{name}")
public CustomerDto getByName(#PathVariable String name) {
return service.getByName()
.orElseThrow(() -> new ResponseStatusException("Customer not found"));
}
#GetMapping
public List<CustomerDto> getAll() {
return service.getAll();
}
}
Don't forget to configure your project to use MapStruct and Lombok together:
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

MapStruct can't find property both in source and target

Source Class looks like:
Data
#Accessors(chain = true)
#Validated
public class OAuth2ClientCreateRequest {
#NotNull
Data data;
#lombok.Data
#Accessors(chain = true)
public static class Data {
#Pattern(regexp = "oauth2_clients")
private String type;
#NotNull
private OAuth2ClientAttributes attributes;
}
#lombok.Data
#Accessors(chain = true)
public static class OAuth2ClientAttributes {
#NotNull #Length(min = 10, max = 256)
private String clientId;
......
Target Class Looks like:
#Accessors(chain = true)
#Getter
#Setter
#ToString
public class OAuth2Client extends BaseEntity<OAuth2Client> implements Serializable {
#NotNull
#Length(min = 10, max = 256)
#JsonProperty
private String clientId;
........
Mapper Class:
#Mapper(componentModel = "spring")
public interface OAuth2ClientMapper {
#Mapping(target = "clientId", source = "attr.clientId")
OAuth2Client convert(OAuth2ClientCreateRequest.OAuth2ClientAttributes attr);
}
Errors I am getting while doing Maven Compile:
[ERROR] ....../OAuth2ClientMapper.java:[14,52] The type of parameter "attr" has no property named "clientId".
[ERROR] ....../OAuth2ClientMapper.java:[14,52] Unknown property "clientId" in result type .....oauth2authserver.domain.entity.OAuth2Client. Did you mean "null"?
Notice that I am using MapStruct with Lombok. Is there any preprocessor related issues here?
In my project in IntelliJ IDE, Lombok was working without adding any annotation preprocessors because Lombok plugin was downloaded through IntelliJ settings.
Then when it comes to adding map-struct dependency in pom.xml, I had to add annotation preprocessor plugin mapstruct-processor in pom.xml. Then Lombok starts unable to work.
Finally adding annotation processors both for Lombok and Map-Struct like below works -
<properties>
<java.version>11</java.version>
<mapstruct.version>1.3.1.Final</mapstruct.version>
<gson.version>2.8.5</gson.version>
</properties>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

getOutputStream() has already been called for this response when fill data in child table with spring boot

I am using spring boot 1.5.10 & spring rest to develop some rest services
I have relation one to many between product and services.
When I fill data in services table and access the service that get me all products(http://localhost:8080/user/products) give me this exception:
Caused by: java.lang.IllegalStateException: getOutputStream() has
already been called for this response.
and repeated json appear in the browser!
If services table is empty: no exception is thrown. I don't know why.
I found a link that discuss the problem but yet I couldn't solve it.
Product entity:
#Entity
#Table(name = "products")
public class Product implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "ID")
private Integer id;
#Basic(optional = false)
#Column(name = "NAME")
private String name;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "productId")
private List<Service> servicesList;
}
Service Entity:
#Entity
#Table(name = "services")
public class Service implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "ID")
private Integer id;
#Basic(optional = false)
#Column(name = "NAME")
private String name;
#Basic(optional = false)
#Column(name = "TYPE")
private int type;
#JoinColumn(name = "PRODUCT_ID", referencedColumnName = "ID")
#ManyToOne(optional = false)
private Product productId;
}
And the ProductController
#Controller
#RequestMapping("user")
#CrossOrigin(origins="http://localhost:4200", allowedHeaders="*")
public class ProductController {
#Autowired
private IProductService productService;
#GetMapping("product/{id}")
public ResponseEntity<Product> getProductById(#PathVariable("id") Integer id) {
Product product = productService.getProductById(id);
return new ResponseEntity<Product>(product, HttpStatus.OK);
}
#GetMapping("products")
public ResponseEntity<List<Product>> getAllProducts() {
List<Product> list = productService.getAllProducts();
return new ResponseEntity<List<Product>>(list, HttpStatus.OK);
}
}
pom.xml is defined as follows.
http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
<groupId>com.app</groupId>
<artifactId>Assignment</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Assignment</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
As discussed in the link you have shared, you need to create a response class to be sent to the client who is calling the API.
For example, you might have a class saying ProductResponse which might look like this.
public class ProductResponse implements Serializable {
public Integer id;
public String name;
public List<Service> servicesList;
}
Now in the controller class, populate create the response as follows.
#GetMapping("product/{id}")
public ResponseEntity<ProductResponse> getProductById(#PathVariable("id") Integer id) {
Product product = productService.getProductById(id);
ProductResponse productResponse = createProductResponse(product);
return new ResponseEntity<ProductResponse>(productResponse, HttpStatus.OK);
}
ProductResponse createProductResponse(Product product) {
ProductResponse productResponse = new ProductResponse();
productResponse.id = product.id;
productResponse.name = product.name;
productResponse.serviceList = product.serviceList;
}
And yes, you need to specify the FetchType.EAGER in the entity class of Product.
#Entity
#Table(name = "products")
public class Product implements Serializable {
// ... Other parameters
#OneToMany(fetch = "FetchType.EAGER", cascade = CascadeType.ALL, mappedBy = "productId")
private List<Service> servicesList;
}
Hope that helps.

Unable to retrieve Spring HATEOAS embedded resource object in case of #ManytoMany relationship and lookup table with extra column

I am unable to retrieve embedded .I am using Spring boot ,spring data rest and spring JPA. I have 3 tables in data base
user
competency
user_competency (join/composite table with extra column)
User
#Entity
#Table(name = "\"user\"", schema = "public")
#JsonIdentityInfo(
generator = ObjectIdGenerators.IntSequenceGenerator.class,
property = "userId")
public class User implements java.io.Serializable {
private Long userId;
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "user_id", unique = true, nullable = false)
public Long getUserId() {
return this.userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
private Set<UserCompetency> userCompetencies = new HashSet<UserCompetency>(0);
#OneToMany(fetch = FetchType.EAGER,cascade = {CascadeType.ALL}, mappedBy = "user")
public Set<UserCompetency> getUserCompetencies() {
return this.userCompetencies;
}
public void setUserCompetencies(Set<UserCompetency> userCompetencies) {
this.userCompetencies = userCompetencies;
}
}
**Competency**
#Entity
#Table(name = "competency", schema = "public")
#JsonIdentityInfo(
generator = ObjectIdGenerators.IntSequenceGenerator.class,
property = "competencyId")
public class Competency implements java.io.Serializable {
private Long competencyId;
private Set<UserCompetency> userCompetencies = new HashSet<UserCompetency>(0);
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "competency_id", unique = true, nullable = false)
public Long getCompetencyId() {
return this.competencyId;
}
public void setCompetencyId(Long competencyId) {
this.competencyId = competencyId;
}
#OneToMany(fetch = FetchType.LAZY, mappedBy = "competency")
public Set<UserCompetency> getUserCompetencies() {
return this.userCompetencies;
}
public void setUserCompetencies(Set<UserCompetency> userCompetencies) {
this.userCompetencies = userCompetencies;
}
}
UserCompetency
#Entity
#Table(name = "user_competency", schema = "public")
#JsonIdentityInfo(
generator =ObjectIdGenerators.IntSequenceGenerator.class,
property = "id")
public class UserCompetency implements java.io.Serializable {
private UserCompetencyId id;
private Level level;
private User user;
private Competency competency;
#EmbeddedId
#AttributeOverrides({
#AttributeOverride(name = "competencyId", column = #Column(name = "competency_id", nullable = false)),
#AttributeOverride(name = "userId", column = #Column(name = "user_id", nullable = false)) })
public UserCompetencyId getId() {
return this.id;
}
public void setId(UserCompetencyId id) {
this.id = id;
}
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "level_id")
public Level getLevel() {
return this.level;
}
public void setLevel(Level level) {
this.level = level;
}
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "user_id", nullable = false, insertable = false, updatable = false)
public User getUser() {
return this.user;
}
public void setUser(User user) {
this.user = user;
}
#ManyToOne(fetch = FetchType.EAGER,cascade=CascadeType.ALL)
#JoinColumn(name = "competency_id", nullable = false, insertable = false, updatable = false)
public Competency getCompetency() {
return this.competency;
}
public void setCompetency(Competency competency) {
this.competency = competency;
}
}
UserCompetencyId
#Embeddable
public class UserCompetencyId implements java.io.Serializable {
private Long competencyId;
private Long userId;
public UserCompetencyId() {
}
public UserCompetencyId(Long competencyId, Long userId) {
this.competencyId = competencyId;
this.userId = userId;
}
#Column(name = "competency_id", nullable = false)
public Long getCompetencyId() {
return this.competencyId;
}
public void setCompetencyId(Long competencyId) {
this.competencyId = competencyId;
}
#Column(name = "user_id", nullable = false)
public Long getUserId() {
return this.userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public boolean equals(Object other) {
if ((this == other))
return true;
if ((other == null))
return false;
if (!(other instanceof UserCompetencyId))
return false;
UserCompetencyId castOther = (UserCompetencyId) other;
return (this.getCompetencyId() == castOther.getCompetencyId()) && (this.getUserId() == castOther.getUserId());
}
}
UserCompetencyRepository
public interface UserCompetencyRepository extends JpaRepository<UserCompetency, UserCompetencyId> {
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>Demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>Demo</name>
<description>Demo api </description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-browser</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-webmvc</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
and I want to perform GET using URI,it return me embedded object and cannot get real value of objects attributes
GET http://localhost:8080/userCompetencies
How Can I get attribute values of User and Competency object where userId=8 Help is required
After implementing suggested Projection Issue still not resolved and here is screen shot
One way would be to use projections like for example:
#Projection(name = "edit" , types = Employee.class)
public interface EditEmployeeProjection {
String getFirstName();
String getLastName();
Set<Project> getProjects();
}
With this the project list will be embedded in the result for http://localhost:8080/api/employee/1?projection=edit
Projections would be used automatically if you add excerptProjection to you repository like described here: How to expose a complete tree structure with Spring Data REST and HATEOAS?
See for example here: https://shinesolutions.com/2015/04/15/spring-data-rest-and-projections/
EDITED
In you case a projection would look like:
#Projection(name = "edit" , types = UserCompetency.class)
public interface UserCompetencyProjection {
User getUser();
Competency getCompetency();
}
With http://localhost:8080/userCompetencies?projection=edit you would then see wanted result.
EDITED 2
The code I used:
Competency.class
#Entity
#Table(name = "competency", schema = "public")
#JsonIdentityInfo(
generator = ObjectIdGenerators.IntSequenceGenerator.class,
property = "competencyId")
public class Competency implements java.io.Serializable {
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "competency_id", unique = true, nullable = false)
private Long competencyId;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "competency")
private List<UserCompetency> userCompetencies = new ArrayList<>();
UserCompetency.class
#Entity
#Table(name = "user_competency", schema = "public")
#JsonIdentityInfo(
generator = ObjectIdGenerators.IntSequenceGenerator.class,
property = "id")
public class UserCompetency implements java.io.Serializable {
#EmbeddedId
#AttributeOverrides({
#AttributeOverride(name = "competencyId", column = #Column(name = "competency_id", nullable = false)),
#AttributeOverride(name = "userId", column = #Column(name = "user_id", nullable = false)) })
private UserCompetencyId id;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "user_id", nullable = false, insertable = false, updatable = false)
private User user;
#ManyToOne(fetch = FetchType.EAGER,cascade = CascadeType.ALL)
#JoinColumn(name = "competency_id", nullable = false, insertable = false, updatable = false)
private Competency competency;
UserCompetencyId.class
#Embeddable
public class UserCompetencyId implements java.io.Serializable {
#Column(name = "competency_id", nullable = false)
private Long competencyId;
#Column(name = "user_id", nullable = false)
private Long userId;
UserCompetencyRepository.class
#RepositoryRestResource(excerptProjection = UserCompetencyProjection.class)
public interface UserCompetencyRepository extends JpaRepository<UserCompetency, UserCompetencyId> {
After Implementing This ,In my case its not working to show desired jason [![enter image description here][2]][2]
It worked out ,actually i was missing annotation of #RepositoryRestResource(excerptProjection = UserCompetencyProjection.class)
on UserCompetencyRepository class now the output look like this I am skipping as it is output , and putting necessary output.

Resources