Mapstruct - How to convert a DTO String parameter to an Entity object? - spring

I'm new to Mapstruct and I'm trying to understand it properly.
What I want to achieve is converting from a DTO String parameter (carModel) to his Entity, retrieve using Service and Repository.
The problem is that Mapper class generated by Mapstruct is trying to inject the Service class with #Autowired annotation, but it's not working. The service is null.
Here's my #Mapper class:
#Mapper(componentModel = "spring", uses = CarModelService.class)
public interface KitMapper extends EntityMapper<KitDTO, Kit> {
KitMapper INSTANCE = Mappers.getMapper(KitMapper.class);
#Mapping(source = "weight", target = "systemWeight")
#Mapping(source = "carModel", target = "carModel")
Kit toEntity(KitDTO kitDTO);
}
public interface EntityMapper<D, E> {
E toEntity(D dto);
List<E> toEntity(List<D> dtoList);
}
The #Service class:
#Service
#Transactional
public class CarModelService {
private final CarModelRepository carModelRepository;
#Transactional(readOnly = true)
public CarModel findByName(String name) {
return carModelRepository.findByName(name).orElse(null);
}
}
The #Repository class:
#Repository
public interface CarModelRepository extends JpaRepository<CarModel, Long> {
Optional<CarModel> findByName(String carModelName);
}
The DTO and Entity classes:
public class KitDTO {
private String id;
private String carModel; // e.g. "Ferrari Monza"
....
}
#Entity
#Table(name = "kit")
public class Kit implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
#Column(name = "id")
private Long id;
#ManyToOne
private CarModel carModel;
...
}
#Entity
#Table(name = "car_model")
public class CarModel implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
#Column(name = "id")
private Long id;
#Column(name = "name")
private String name;
...
}
The build work properly but the application stop when I try to use the Mapper. It says that carModelService is null.
Here's the mapper generated implementation class:
#Component
public class KitMapperImpl implements KitMapper {
#Autowired // <-- this seems not working
private CarModelService carModelService;
#Override
public Kit toEntity(KitDTO kitDTO) {
if ( kitDTO == null ) {
return null;
}
Kit kit = new Kit();
kit.setSystemWeight( String.valueOf( kitDTO.getWeight() ) );
kit.carModel( carModelService.findByName(kitDTO.getCarModel()) ); // <-- carModelService is null!
// other setters
return kit;
}
}
I've tried many things, using Decorator, #Context, expression, inject the #Mapper class into the #Service class.
I've found many questions but actually no one helped me:
Mapstruct - How can I inject a spring dependency in the Generated Mapper class
#Service Class Not Autowired in org.mapstruct.#Mapper Class
MapStruct mapper not initialized with autowired when debug
Any help would be appreciated! Thanks in advance!

Found the solution!
Instead of calling directly the Mapper method toEntity() from the #RestController class, I injected the mapper in the CarModelService class and created a method that call the mapper.
In this way the flow is:
Controller --> Service --> Mapper
#Service
#Transactional
public class KitService {
private final KitRepository kitRepository;
private final KitSearchRepository kitSearchRepository;
private final KitMapper kitMapper; // <-- mapper declaration
public KitService(KitRepository kitRepository, KitSearchRepository kitSearchRepository, KitMapper kitMapper) {
this.kitRepository = kitRepository;
this.kitSearchRepository = kitSearchRepository;
this.kitMapper = kitMapper; // <-- mapper initilization
}
// here the method which calls mapper
public Kit convertDTOToEntity(KitDTO kitDTO) {
return kitMapper.toEntity(kitDTO);
}
In this way, the generated class by Mapstruct doesn't give error on the CarModelService.
Seems like this approach is the only way to achieve this, create a king of "bridge" between services and mappers.
(You can use also the #Autowired annotation instead of constructor)

Can you please share the error message?
From the information that you shared, I can see the carModel in KitDto is String and in Entity is CarModel class. Not sure how mapstruct's auto generated implementation class implemented this: kit.carModel( carModelService.findByName(kitDTO.getCarModel()) );.
But I would like to share another approach,Don't know this is a best practice or not. In this approach you can create a abstarct class of mapper, in which you can #Autowired repository can manually add those mapping.
I shared the snippet for it. Hopefully this will help you.
#Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public abstract class ProductMapper {
#Autowired
private CarModelService carModelService;
public abstract Kit convertDTOToEntity(KitDTO kitDTO);
public Kit toEntity(KitDTO kitDTO);
{
Kit kit = convertDTOToEntity(kitDTO);
kit.setCarModel(carModelService.findByName(kitDTO.getCarModel()));
return kit;
}
}
Curious about the other approaches, will follow this thread. We can discuss the best practices

Related

JPA findTopBy cannot work on spring boot 2.6.3

I am studying spring-boot and JPA. the spring-boot version is 2.6.3, which is the latest current version.
I defined my entity class like:
#Entity
public class Author implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private int age;
private String name;
private String genre;
// ignore the getters, setters and toString()
And defined with one projection DTO interface.
public interface AuthorDto {
public String getName();
public int getAge();
}
then defined the repository as follows:
#Repository
public interface AuthorRepository extends JpaRepository<Author, Long> {
Author findFirstByGenre(String genre);
AuthorDto findTopByGenre(String genre);
}
the method findFirstByXXX worked well, but the second method, AuthorDto findTopByGenre(String genre);, could not work well, and was ended up with the following error in the log:
Reason: Failed to create query for method public abstract com.bookstore.dto.AuthorDto com.bookstore.repository.AuthorRepository.findTopByGenre(java.lang.String)! Cannot invoke "java.lang.Class.isInterface()" because "typeToRead" is null; nested exception is java.lang.IllegalArgumentException: Failed to create query for method public abstract com.bookstore.dto.AuthorDto com.bookstore.repository.AuthorRepository.findTopByGenre(java.lang.String)! Cannot invoke "java.lang.Class.isInterface()" because "typeToRead" is null
I tested the same code on spring-boot 2.1.4.RELEASE, it could work well.
I tried to find any official documents on this, but didn't find useful thread on this.

How can I add a tenant condition to Spring Data JPA Default and Dervied Queries

I have a Springboot Application with Repositories having Spring Data JPA Queries like findOne, findAll and also derived ones like findByID or findByName etc.
What I want to achieve is multitenancy. All entities have an "account_id" column which holds the tenant.
How do I add a filter like "account_id" to all the queries metioned above without using derived queries that contains those name slike findIdAndAccountid (which would be findone)
#Repository
public interface CategoryRepository extends JpaRepository<Category, Long> {
Category findByName(String name);
}
Here's the corresponding entity
#Entity
#Table(name = "unit")
#Data
public class Unit {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String name;
#Column(name = "account_id")
private Long account_id;
}
I know most people use schemas as tenant separation but that's impossible for me. Is there a way (I didn't find one) to add such a tenant filter condition on those queries without writing NamedQueries or using DerivedQueries. An elegeant solution like annotate the repository or entity or maybe the queries that all queries should add the additional filter "account_id"?
You can add Where clause on your Entity classes (Didnt had time to test )
#Entity
#Table(name = "unit")
#Data
#Where(clause = "account_id= :account_id")
public class Unit {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String name;
#Column(name = "account_id")
private Long account_id;
}
Update and Solution
1. Create a Filter & FilterDef on the entity like so
#FilterDef(name="accountFilter", parameters=#ParamDef( name="accountId", type="long" ) )
#Filters( {
#Filter(name="accountFilter", condition=":accountId = account_id")
} )
public class Category {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String name;
#Column(name = "account_id")
private Long account_id;
}
enable filtering in the controller by autowiring entitymanager, writing a method to enable the filter and activate the filter in #ModelAttribute for each request
#RestController
#RequestMapping(path = "/categories",produces = MediaType.APPLICATION_JSON_VALUE )
public class CategoryController {
private final CategoryRepository repository;
#Autowired
private EntityManager entityManager;
CategoryController(CategoryRepository repository) {
this.repository = repository;
}
private void activateFilter() {
Session session = entityManager.unwrap(Session.class);
Filter filter = session.enableFilter("accountFilter");
filter.setParameter("accountId", Long.valueOf(TenantContext.getCurrentTenant()));
}
#ModelAttribute
public void initFilter() {
activateFilter();
}
... your rest methods here
}

SpringData and Elasticsearch - java.lang.StackOverflowError while using ignoreFields

I am using simple relationship (spring data elasticsearch) but springboot is throwing StackOverflowError
#Document(indexName = "users", type = "user")
public class User {
#Id
private String id;
#Field(type= FieldType.Nested,ignoreFields={"users"})
private Set<Group> groups = new HashSet<Group>();
}
#Document(indexName = "groups", type = "group")
public class Group {
#Id
String id;
#Field(type = FieldType.Nested, ignoreFields ={"groups"})
private Set<User> users = new HashSet<User>();
}
public interface UserRepository extends ElasticsearchRepository<User, String>{
}
public interface GroupRepository extends ElasticsearchRepository<Group, String> {
}
Any Idea what is problem?
Code is from https://github.com/spring-projects/spring-data-elasticsearch/tree/master/src/test/java/org/springframework/data/elasticsearch/entities
Thanks
Rajan
As you said: spring-boot is throwing StackOverflowError.
It's because of no safeguards or detecting circular dependencies in spring-boot.
Look at spring-boot source: MappingBuilder.java, there's loop that will never break when you have class A that have a #Field annotation referencing class B, that have #Field annotation referencing class A.

How to get data from a table by entity class name using Spring Data JPA

I have a base entity class BaseDictionary:
#Entity
#Inheritance
public abstract class BaseDictionary {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
#Column(name = "code")
protected String code;
#Column(name = "is_enabled")
protected Boolean enabled = true;
}
any child classes
#Entity
#Table(name = DB.DIC_RIGHTS_TYPE, schema = DB.SCHEMA_DIC)
public class DicRightsType extends BaseDictionary {}
#Entity
#Table(name = DB.DIC_ROLES_TYPE, schema = DB.SCHEMA_DIC)
public class DicRolesType extends BaseDictionary {}
There are many child classes like this.
Given an entity class name like DicRightsType I would like to get data from the table associated with the entity of that name. How is it possible to implement?
I wanted to use JPA repositories like this: Using generics in Spring Data JPA repositories but this does not suit my case because I only have the name of the entity class at runtime and do not know how to create a dynamic repository for the class.
You can write your own JpaRepository implementation to achieve this.
Step 1: A repository registry
class RepositoryRegistrar {
private static final Map<Class<T>, Repository<T, Serializable>> DICTIONARY = new HashMap<>();
public static void register(Class<T> entityClass, Repository<T, Serializable> repository) {
DICTIONARY.put(entityClass, repository);
}
public static Repository<T, Serializable> get(Class<T> entityClass) {
return DICTIONARY.get(entityClass);
}
}
Step 2: Custom JpaRepository implementation to populate the registry
#NoRepositoryBean
class RegisteringJpaRepository<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> {
public RegisteringJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {{
super(entityInformation, entityManager);
RepositoryRegistrar.register(entityInformation.getJavaType(), this);
}
}
You will have to tweak the configuration to use your custom implementation. Details for this are provided in the Spring Data JPA documentation.
Step 3: Obtain repository references from the registrar
Repository<?, ?> getRepository(String entityClassName) {
return RepositoryRegistrar.get(Class.forName(entityClassName));
}

Spring Data Neo4J #Indexed(unique = true) not working

I'm new to Neo4J and I have, probably an easy question.
There're NodeEntitys in my application, a property (name) is annotated with #Indexed(unique = true) to achieve the uniqueness like I do in JPA with #Column(unique = true).
My problem is, that when I persist an entity with a name that already exists in my graph, it works fine anyway.
But I expected some kind of exception here...?!
Here' s an overview over basic my code:
#NodeEntity
public abstract class BaseEntity implements Identifiable
{
#GraphId
private Long entityId;
...
}
public class Role extends BaseEntity
{
#Indexed(unique = true)
private String name;
...
}
public interface RoleRepository extends GraphRepository<Role>
{
Role findByName(String name);
}
#Service
public class RoleServiceImpl extends BaseEntityServiceImpl<Role> implements
{
private RoleRepository repository;
#Override
#Transactional
public T save(final T entity) {
return getRepository().save(entity);
}
}
And this is my test:
#Test
public void testNameUniqueIndex() {
final List<Role> roles = Lists.newLinkedList(service.findAll());
final String existingName = roles.get(0).getName();
Role newRole = new Role.Builder(existingName).build();
newRole = service.save(newRole);
}
That's the point where I expect something to go wrong!
How can I ensure the uniqueness of a property, without checking it for myself??
P.S.: I'm using neo4j 1.8.M07, spring-data-neo4j 2.1.0.BUILD-SNAPSHOT and Spring 3.1.2.RELEASE.
I walked into the same trap... as long as you create new entities, you will not see the exception - the last save()-action wins the battle.
Unfortunately, the DataIntegrityViolationException will be raised only in case of update an existing entity!
A detailed description of that behaviour can be found here:
http://static.springsource.org/spring-data/data-graph/snapshot-site/reference/html/#d5e1035
If you are using SDN 3.2.0+ use the failOnDuplicate attribute:
public class Role extends BaseEntity
{
#Indexed(unique = true, failOnDuplicate = true)
private String name;
...
}

Resources