How to properly convert domain entities to DTOs while considering scalability & testability
Asked Answered
S

6

46

I have read several articles and Stackoverflow posts for converting domain objects to DTOs and tried them out in my code. When it comes to testing and scalability I am always facing some issues. I know the following three possible solutions for converting domain objects to DTOs. Most of the time I am using Spring.

Solution 1: Private method in the service layer for converting

The first possible solution is to create a small "helper" method in the service layer code which is convertig the retrieved database object to my DTO object.

@Service
public MyEntityService {

  public SomeDto getEntityById(Long id){
    SomeEntity dbResult = someDao.findById(id);
    SomeDto dtoResult = convert(dbResult);
    // ... more logic happens
    return dtoResult;
  }

  public SomeDto convert(SomeEntity entity){
   //... Object creation and using getter/setter for converting
  }
}

Pros:

  • easy to implement
  • no additional class for convertion needed -> project doesn't blow up with entities

Cons:

  • problems when testing, as new SomeEntity() is used in the privated method and if the object is deeply nested I have to provide a adequate result of my when(someDao.findById(id)).thenReturn(alsoDeeplyNestedObject) to avoid NullPointers if convertion is also dissolving the nested structure

Solution 2: Additional constructor in the DTO for converting domain entity to DTO

My second solution would be to add an additional constructor to my DTO entity to convert the object in the constructor.

public class SomeDto {

 // ... some attributes

 public SomeDto(SomeEntity entity) {
  this.attribute = entity.getAttribute();
  // ... nesting convertion & convertion of lists and arrays
 }

}

Pros:

  • no additional class for converting needed
  • convertion hided in the DTO entity -> service code is smaller

Cons:

  • usage of new SomeDto() in the service code and therefor I have to provide the correct nested object structure as a result of my someDao mocking.

Solution 3: Using Spring's Converter or any other externalized Bean for this converting

If recently saw that Spring is offering a class for converting reasons: Converter<S, T> but this solution stands for every externalized class which is doing the convertion. With this solution I am injecting the converter to my service code and I call it when i want to convert the domain entity to my DTO.

Pros:

  • easy to test as I can mock the result during my test case
  • separation of tasks -> a dedicated class is doing the job

Cons:

  • doesn't "scale" that much as my domain model grows. With a lot of entities I have to create two converters for every new entity (-> converting DTO entitiy and entitiy to DTO)

Do you have more solutions for my problem and how do you handle it? Do you create a new Converter for every new domain object and can "live" with the amount of classes in the project?

Thanks in advance!

Shurlock answered 16/12, 2017 at 6:5 Comment(2)
Option 4 using something like MapStruct to generate the mapping code.Corollaceous
Do you want to test your converters separately ?Miniver
P
29

Solution 1: Private method in the service layer for converting

I guess Solution 1 will not not work well, because your DTOs are domain-oriented and not service oriented. Thus it will be likely that they are used in different services. So a mapping method does not belong to one service and therefore should not be implemented in one service. How would you re-use the mapping method in another service?

The 1. solution would work well if you use dedicated DTOs per service method. But more about this at the end.

Solution 2: Additional constructor in the DTO for converting domain entity to DTO

In general a good option, because you can see the DTO as an adapter to the entity. In other words: the DTO is another representation of an entity. Such designs often wrap the source object and provide methods that give you another view on the wrapped object.

But a DTO is a data transfer object so it might be serialized sooner or later and send over a network, e.g. using spring's remoting capabilities. In this case the client that receives this DTO must deserialize it and thus needs the entity classes in it's classpath, even if it only uses the DTO's interface.

Solution 3: Using Spring's Converter or any other externalized Bean for this converting

Solution 3 is the solution that I also would prefer. But I would create a Mapper<S,T> interface that is responsible for mapping from source to target and vice versa. E.g.

public interface Mapper<S,T> {
     public T map(S source);
     public S map(T target);
}

The implementation can be done using a mapping framework like modelmapper.


You also said that a converter for each entity

doesn't "scale" that much as my domain model grows. With a lot of entities I have to create two converters for every new entity (-> converting DTO entitiy and entitiy to DTO)

I doupt that you only have to create 2 converter or one mapper for one DTO, because your DTO is domain-oriented.

As soon as you start to use it in another service you will recognize that the other service usually should or can not return all values that the first service does. You will start to implement another mapper or converter for each other service.

This answer would get to long if I start with pros and cons of dedicated or shared DTOs, so I can only ask you to read my blog pros and cons of service layer designs.

EDIT

About the third solution: where do you prefer to put the call for the mapper?

In the layer above the use cases. DTOs are data transfer objects, because they pack data in data structures that are best for the transfer protocol. Thus I call that layer the transport layer. This layer is responsible for mapping use case's request and result objects from and to the transport representation, e.g. json data structures.

EDIT

I see you're ok with passing an entity as a DTO constructor parameter. Would you also be ok with the opposite? I mean, passing a DTO as an Entity constructor parameter?

A good question. The opposite would not be ok for me, because I would then introduce a dependency in the entity to the transport layer. This would mean that a change in the transport layer can impact the entities and I don't want changes in more detailed layers to impact more abstract layers.

If you need to pass data from the transport layer to the entity layer you should apply the dependency inversion principle.

Introduce an interface that will return the data through a set of getters, let the DTO implement it and use this interface in the entities constructor. Keep in mind that this interface belongs to the entity's layer and thus should not have any dependencies to the transport layer.

                                interface
 +-----+  implements     ||   +------------+   uses  +--------+
 | DTO |  ---------------||-> | EntityData |  <----  | Entity |
 +-----+                 ||   +------------+         +--------+
Pyrenees answered 17/12, 2017 at 9:34 Comment(5)
About the third solution: where do you prefer to put the call for the mapper?Dahliadahlstrom
@Dahliadahlstrom I updated my answer and hope it answers your question.Keitloa
I see you're ok with passing an entity as a DTO constructor parameter. Would you also be ok with the opposite? I mean, passing a DTO as an Entity constructor parameter?Recalcitrate
@LuisGouveia To give you a more detailed answer I updated my one.Keitloa
You Mapper interface will throw an error "both methods have same erasure". <S extends XX, T extends YY> should fix it. Otherwise MapStruct and its @Mapping annotation should get rid of the boiler plateAdabelle
F
17

I like the third solution from the accepted answer.

Solution 3: Using Spring's Converter or any other externalized Bean for this converting

And I create DtoConverter in this way:

BaseEntity class marker:

public abstract class BaseEntity implements Serializable {
}

AbstractDto class marker:

public class AbstractDto {
}

GenericConverter interface:

public interface GenericConverter<D extends AbstractDto, E extends BaseEntity> {

    E createFrom(D dto);

    D createFrom(E entity);

    E updateEntity(E entity, D dto);

    default List<D> createFromEntities(final Collection<E> entities) {
        return entities.stream()
                .map(this::createFrom)
                .collect(Collectors.toList());
    }

    default List<E> createFromDtos(final Collection<D> dtos) {
        return dtos.stream()
                .map(this::createFrom)
                .collect(Collectors.toList());
    }

}

CommentConverter interface:

public interface CommentConverter extends GenericConverter<CommentDto, CommentEntity> {
}

CommentConveter class implementation:

@Component
public class CommentConverterImpl implements CommentConverter {

    @Override
    public CommentEntity createFrom(CommentDto dto) {
        CommentEntity entity = new CommentEntity();
        updateEntity(entity, dto);
        return entity;
    }

    @Override
    public CommentDto createFrom(CommentEntity entity) {
        CommentDto dto = new CommentDto();
        if (entity != null) {
            dto.setAuthor(entity.getAuthor());
            dto.setCommentId(entity.getCommentId());
            dto.setCommentData(entity.getCommentData());
            dto.setCommentDate(entity.getCommentDate());
            dto.setNew(entity.getNew());
        }
        return dto;
    }

    @Override
    public CommentEntity updateEntity(CommentEntity entity, CommentDto dto) {
        if (entity != null && dto != null) {
            entity.setCommentData(dto.getCommentData());
            entity.setAuthor(dto.getAuthor());
        }
        return entity;
    }

}
Fundus answered 20/12, 2017 at 10:27 Comment(0)
D
12

I ended up NOT using some magical mapping library or external converter class, but just adding a small bean of my own which has convert methods from each entity to each DTO I need. The reason is that the mapping was:

either stupidly simple and I would just copy some values from one field to another, perhaps with a small utility method,

or was quite complex and would be more complicated to write down in the custom parameters to some generic mapping library, compared to just writing out that code. This is for example in the case where the client can send JSON but under the hood this is transformed into entities, and when the client retrieves the parent object of these entities again, it's converted back into JSON.

This means I can just call .map(converter::convert) on any collection of entities to get back a stream of my DTO's.

Is it scalable to have it all in one class? Well the custom configuration for this mapping would have to be stored somewhere even if using a generic mapper. The code is generally extremely simple, except for a handful of cases, so I'm not too worried about this class exploding in complexity. I'm also not expecting to have dozens more entities, but if I did I might group these converters in a class per subdomain.

Adding a base class to my entities and DTO's so I can write a generic converter interface and implement it per class just isn't needed (yet?) either for me.

Decoder answered 14/5, 2018 at 3:17 Comment(0)
B
9

In my opinion the third solution is the best one. Yes for each entity you'll have to create a two new convert classes but when you come time for testing you won't have a lot of headaches. You should never chose the solution which will cause you to write less code at the begining and then write much more when it comes to testing and maintaining that code.

Botsford answered 16/12, 2017 at 12:20 Comment(0)
M
3

Another point is , if you use the second approach and your entity has lazy dependencies, your Dto can't understand if dependency is loaded unless you inject EntityManager into the Dto and use it to check if dependency was loaded. I don't like this approach cause Dto shouldn't know anything about EntityManager. As a solution I personally prefer Converters but at the same time I prefer to have multiple Dto classes for the same entity . For example If I am 100 % sure that User Entity will be loaded without corresponding Company , then there has to be a UserDto that doesn't have CompanyDto as a field. At the same time If I know that UserEntity will be loaded with correlated Company , then I will use aggregate pattern , something like a UserCompanyDto class that contains UserDto and CompanyDto as parameters

Micronucleus answered 20/5, 2021 at 17:56 Comment(0)
E
0

On my side I prefer using option 3 with a third party library such as modelmapper or mapstruct. Also I use it through interface in an util package, because I don't want any external tool or library to interact directly with my code.

  1. Definition:

    public interface MapperWrapper {
    
     <T> T performMapping(Object source, Class<T> destination);
    }
    
    @Component
    public class ModelMapperWrapper implements MapperWrapper {
    
        private ModelMapper mapper;
    
        public ModelMapperWrapper() {
            this.mapper = new ModelMapper();
        }
    
        @Override
        public <T> T performMapping(Object source, Class<T> 
           destination) {
         mapper.getConfiguration()
         .setMatchingStrategy(MatchingStrategies.STRICT);
            return mapper.map(source, destination);
        }
    }
    

Then after I can test it easily:

  1. Testing:

    @SpringJUnitWebConfig(TestApplicationConfig.class)
    class ModelMapperWrapperTest implements WithAssertions {
    
     private final MapperWrapper mapperWrapper;
    
     @Autowired
     public ModelMapperWrapperTest(MapperWrapper mapperWrapper) {
         this.mapperWrapper = mapperWrapper;
     }
    
     @BeforeEach
     void setUp() {
     }
    
     @Test
     void givenModel_whenMapModelToDto_thenReturnsDto() {
    
         var model = new DummyModel();
         model.setId(1);
         model.setName("DUMMY_NAME");
         model.setAge(25);
    
         var modelDto = mapperWrapper.performMapping(model, DummyModelDto.class);
    
         assertAll(
                 () -> assertThat(modelDto.getId()).isEqualTo(String.valueOf(model.getId())),
                 () -> assertThat(modelDto.getName()).isEqualTo(model.getName()),
                 () -> assertThat(modelDto.getAge()).isEqualTo(String.valueOf(model.getAge()))
         );
    
     }
    
     @Test
     void givenDto_whenMapDtoToModel_thenReturnsModel() {
    
         var modelDto = new DummyModelDto();
         modelDto.setId("1");
         modelDto.setName("DUMMY_NAME");
         modelDto.setAge("25");
    
         var model = mapperWrapper.performMapping(modelDto, DummyModel.class);
    
         assertAll(
                 () -> assertThat(model.getId()).isEqualTo(Integer.valueOf(modelDto.getId())),
                 () -> assertThat(model.getName()).isEqualTo(modelDto.getName()),
                 () -> assertThat(model.getAge()).isEqualTo(Integer.valueOf(modelDto.getAge()))
         );
        }
    
    }
    

After that it can be very easy to use another mapper library. I should have created an abstract factory, or strategy pattern also.

Elfie answered 22/2, 2023 at 21:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.