#Clean code. It is okay to use repository inside Entity-DTO mapper?

1 messages · Page 1 of 1 (latest)

icy olive
#
@Component
@RequiredArgsConstructor
public class MaterialUnitMapper {

  private final StockContainerRepository stockContainerRepository;

  public MaterialUnit toEntity(MaterialUnitDTO dto) {
        MaterialUnit unit = new MaterialUnit();

        unit.setName(dto.getName());

        StockContainer stockFromDb = stockContainerRepository.findById(
            dto.getStockContainerId()
          );

        unit.setStockContainer(stockFromDb);

        return unit;
    }
}

Is it okay to hold repository inside mapper? I wonder wether repository should be accessable only from service layer.

carmine yewBOT
#

<@&987246883653156906> please have a look, thanks.

#

Clean code. It is okay to use repository inside Entity-DTO mapper?

#

Changed the title to Clean code. It is okay to use repository inside Entity-DTO mapper?.

primal trout
icy olive
primal trout
#

dunno if thats better. that trips people as well, seeing mapping done outside of the mapper

icy olive
#
@Component
public class MaterialUnitMapper {

  public MaterialUnit toEntity(MaterialUnitDTO dto) {

        return new MaterialUnit(dto.getName());
    }
}

@Service
@RequiredArgsConstructor
public class MaterialUnitService {

  private final MaterialUnitMapper mapper;
  private final MaterialUnitRepository unitRepository;
  private final StockContainerRepository containerRepository;
  
  public MaterialUnit toEntity(MaterialUnitDTO dto) {
    MaterialUnit unit = mapper.toEntity(dto);

    StockContainer stockFromDb = containerRepository.findById(
            dto.getStockContainerId()
          );

    unit.setStockContainer(stockFromDb);
  }
}
icy olive
primal trout
#

thats worse bc now ur mapper spits out unfinished data

#

if u want to go that route u should introduce another class

#

so that the service doesnt just use a setter but maps to another class instead

#

preventing people from accidentally using the result from the mapper directly

#

but imo the main problem here is design-wise to question why u even need a mapper to decorate sth with data from a repo. the mapper is supposed to map from a foreign data model to the data model u want to use in ur application (or the other way around). so why do u need to add sth from the DB to it.

icy olive
primal trout
#

why is this DB thing even considered part of the dto

#

why doesnt the service that needs to work with the DTO get hands on the thing from the DB and it then wont be part of the DTO

icy olive
#

DTO hold unit name and stockContainerId. Entity have name and StockContainer stockContainer

#
public class MaterialUnit {

    @Id
    @Nullable
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "stock_container_id")
    @JsonIgnore
    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    private StockContainer stockContainer;
}

public class MaterialUnitDTO {

    private Long id;
    @NotBlank
    private String name;
    private Long stockContainerId;
}
icy olive
#

But what if i want create new MaterialUnit? I post request body {"name": "Unit1", "stockContainerId": "7"}

@RestController
@RequiredArgsConstructor
public class MaterialUnitController() {
  
  private final MaterialUnitService service;

  public void create(@RequestBody MaterialUnitDTO dto) {
    service.create(dto);
  }
}

@Service
@RequiredArgsConstructor
public class MaterialUnitService {

  private final MaterialUnitMapper mapper;
  private final MaterialUnitRepository unitRepository;
  
  public void create(MaterialUnitDTO dto) {
    MaterialUnit unit = mapper.toEntity(dto);

    StockContainer stockFromDb = containerRepository.findById(
            dto.getStockContainerId()
          );

    unit.setStockContainer(stockFromDb);

    repository.save(unit);
  }
}
#

In both cases I have to somehow get the container from the database to check if container with id=7 exist

primal trout
#

to me what ur doing isnt "mapping" anymore. the point of a mapper is to create a ACL. not to add business logic to ur data.
when u want to transfer a Person class coming from another service/api into the Person class u use internally but that one has int amountOfFriends, then thats not the task of a mapper anymore

#

cause u cant answer that without complex business logic

#

its a design problem. one that requires stepping back

#

one that probably needs 3 distinct DTO classes instead

#

i cant come up with good names now, but essentially: PersonFromOtherService, PersonForOurUse, PersonWithFriendsData

#

and the mapper would take care of going PersonFromOtherService -> PersonForOurUse to create a ACL

#

and PersonWithFriendsData is done by a service, utilizing the DB and whatnot

#

its not a "mapping" anymore, its complex business logic

#

but if u cant find a proper setup where doing sth like that makes sense and id have to pick between the two evils u offered, id probably put the DB into the mapper

icy olive
#

The problem became a little clearer

#

So in my case if i understand correctly. If i wanna create new MaterialUnit, i should create something like second DTO (MaterialUnitForSaveDTO) with second mapper?

icy olive
# primal trout but if u cant find a proper setup where doing sth like that makes sense and id h...

I saw code of experienced guy who has similar problem with mine but he use UserService instead UserRepository

@Component
@RequiredArgsConstructor
public class CarRentMapper {
    private final UserMapper userMapper;
    private final UserService userService;

    CarRentDto toDto(CarRent carRent) {
        return new CarRentDto(carRent.getId(),
                userMapper.toDto(carRent.getUser()),
                carRent.getRentDate(),
                carRent.getEndDate(),
                carRent.getPrice()
        );
    }

    CarRent toEntity(CarRentDto carRentDto) {
        User user = userService.findUser(carRentDto.userId())
            .orElseThrow(() -> new IllegalArgumentException("User with ID "                           + carRentDto.userId() + " not found"));
        return new CarRent (
                user,
                carRentDto.getRentDate(),
                carRentDto.getEndDate(),
                carRentDto.getPrice(),
                carRentDto.getStartDistance(),
                carRentDto.getEndDistance()
        );
    }
}

But still business logic is inside mapper

icy olive
#

@primal trout Sorry for ping but I think I found a better solution sending StockContainer to function in mapper, can you take a look?

@Service
@RequiredArgsConstructor
class MaterialUnitService {

    private final MaterialUnitRepository repository;
    private final MaterialUnitMapper mapper;
    private final StockContainerService stockContainerService;

    @Override
    public MaterialUnitDTO create(MaterialUnitDTO dto) {

        StockContainer stockContainer = null;
        if (dto.getStockContainerId() != null) {
            stockContainer = stockContainerService.findById(dto.getStockContainerId());
        }

        MaterialUnit entity = mapper.toEntity(dto, stockContainer);
        MaterialUnit savedEntity = repository.save(entity);
        return mapper.toDTO(savedEntity);
    }
}
#
@Component
@RequiredArgsConstructor
public class MaterialUnitMapper {

    private final StockContainerService stockContainerService;

    public MaterialUnit toEntity(MaterialUnitDTO dto, StockContainer stockContainer) {
        MaterialUnit unit = new MaterialUnit();

        unit.setId(dto.getId());
        unit.setExternalId(dto.getExternalId());
        unit.setName(dto.getName());
        unit.setLocation(dto.getLocation());
        unit.setStockContainer(stockContainer);

        return unit;
    }
}
primal trout
#

the general approach is better id say, yes.
the problem im having is that its still the same type

#

public MaterialUnitDTO create(MaterialUnitDTO dto) {

#

ur DTO now has a field that is sometimes null, sometimes not null

#

this is dangerous

#

ull run into bugs down the line with that design

#

it would be better if u can separate these two from each other

#

for example by having 2 distinct classes

#

one that never has a container and one that always has one

#

that said, there is a bug in this logic currently:

#
StockContainer stockContainer = null;
if (dto.getStockContainerId() != null) {
  stockContainer = stockContainerService.findById(dto.getStockContainerId());
}

MaterialUnit entity = mapper.toEntity(dto, stockContainer);
#

stockContainer remains null if the dto already had a container

#

in which case ur mapper crashes

#

u probably intended to do it like this instead:

#
StockContainer stockContainer = dto.getStockContainerId();
if (stockContainer != null) {
  stockContainer = stockContainerService.findById(dto.getStockContainerId());
}

MaterialUnit entity = mapper.toEntity(dto, stockContainer);
#

oh, and it probably should be == null, not != null

#

but yeah, details. the approach itself looks better

icy olive
primal trout
#

dont worry. these bugs happen quickly and u should rather blame the design than ur brain

#

objects with data that is sometimes null, sometimes not, quickly lead to logic bugs

icy olive
#

Thanks for help, i think that's all. I'll leave thread if someone wanna read and have similar problem

worldly tendon
#

with JPA having a repository wired is usually not necessary because JPA can make the repo call itself if you try getting a field which is mapped as a relationship

#

or, in the case of 1<->1 relationships, you can preload them in advance