Gu铆a de MapStruct en Java – Biblioteca de mapeo avanzado

    Introducci贸n

    A medida que los microservicios y las aplicaciones distribuidas se apoderan r谩pidamente del mundo del desarrollo, la integridad y la seguridad de los datos son m谩s importantes que nunca. Un canal de comunicaci贸n seguro y una transferencia de datos limitada entre estos sistemas d茅bilmente acoplados son primordiales. La mayor铆a de las veces, el usuario final o el servicio no necesita acceder a la totalidad de los datos de un modelo, sino solo a algunas partes espec铆ficas.

    Los objetos de transferencia de datos (DTO) se aplican regularmente en estas aplicaciones. Los DTO son solo objetos que contienen la informaci贸n solicitada de otro objeto. Normalmente, la informaci贸n tiene un alcance limitado. Dado que los DTO son un reflejo de los objetos originales, los mapeadores entre estas clases juegan un papel clave en el proceso de conversi贸n.

    En este art铆culo, nos sumergiremos en MapStruct , un mapeador extenso para Java Beans.

    Tabla de contenido:

    • MapStruct
    • Asignaciones b谩sicas
    • Asignaci贸n de diferentes campos de origen y destino
      • Diferentes nombres de propiedad
      • Varias clases de fuentes
    • Asignaci贸n de entidades secundarias
    • Actualizaci贸n de instancias existentes
    • Inyecci贸n de dependencia
    • Mapeo de enumeraciones
    • Mapeo de tipos de datos
    • Agregar m茅todos personalizados
    • Creaci贸n de mapeadores personalizados
    • @BeforeMapping y @AfterMapping
    • Agregar valores predeterminados
    • Agregar expresiones de Java
    • Manejo de excepciones durante el mapeo
      • Heredar configuraci贸n
      • Heredar configuraci贸n inversa

    MapStruct

    MapStruct es un generador de c贸digo de c贸digo abierto basado en Java que crea c贸digo para implementaciones de mapas.

    Utiliza el procesamiento de anotaciones para generar implementaciones de clases de mapeadores durante la compilaci贸n y reduce en gran medida la cantidad de c贸digo repetitivo que se escribir铆a regularmente a mano.

    Dependencias de MapStruct

    Si est谩 usando Maven, instale MapStruct agregando la dependencia:

    <dependencies>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${org.mapstruct.version}</version>
        </dependency>
    </dependencies>
    

    Esta dependencia importar谩 las anotaciones centrales de MapStruct. Dado que MapStruct funciona en tiempo de compilaci贸n y est谩 adjunto a constructores como Maven y Gradle, tambi茅n tendremos que agregar un complemento a <build>:

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>
    

    Si est谩 utilizando Gradle, instalar MapStruct es tan simple como:

    plugins {
        id 'net.ltgt.apt' version '0.20'
    }
    
    apply plugin: 'net.ltgt.apt-idea'
    apply plugin: 'net.ltgt.apt-eclipse'
    
    dependencies {
        compile "org.mapstruct:mapstruct:${mapstructVersion}"
        annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
    }
    

    El net.ltgt.aptcomplemento es responsable del procesamiento de anotaciones. Puede aplicar los complementos apt-ideay apt-eclipseseg煤n su IDE.

    Puede consultar la 煤ltima versi贸n en Maven Central .

    Asignaciones b谩sicas

    Comencemos con un mapeo b谩sico. Tendremos un Doctormodelo y un DoctorDto. Sus campos tendr谩n los mismos nombres para nuestra conveniencia:

    public class Doctor {
        private int id;
        private String name;
    }
    

    Y:

    public class DoctorDto {
        private int id;
        private String name;
    }
    

    Ahora, para hacer un mapeador entre estos dos, crearemos una DoctorMapperinterfaz. Al @Mapperanotarlo con , MapStruct sabe que este es un mapeador entre nuestras dos clases:

    @Mapper
    public interface DoctorMapper {
        DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
        DoctorDto toDto(Doctor doctor);
    }
    

    Tenemos una INSTANCEde DoctorMappertipo. Este ser谩 nuestro “punto de entrada” a la instancia una vez que generemos la implementaci贸n.

    Hemos definido un toDto()m茅todo en la interfaz, que acepta una Doctorinstancia y devuelve una DoctorDtoinstancia. Esto es suficiente para que MapStruct sepa que nos gustar铆a mapear una Doctorinstancia a una DoctorDtoinstancia.

    Cuando construimos / compilamos la aplicaci贸n, el complemento del procesador de anotaciones MapStruct tomar谩 la DoctorMapperinterfaz y generar谩 una implementaci贸n para ella:

    public class DoctorMapperImpl implements DoctorMapper {
        @Override
        public DoctorDto toDto(Doctor doctor) {
            if ( doctor == null ) {
                return null;
            }
            DoctorDtoBuilder doctorDto = DoctorDto.builder();
    
            doctorDto.id(doctor.getId());
            doctorDto.name(doctor.getName());
    
            return doctorDto.build();
        }
    }
    

    La DoctorMapperImplclase ahora contiene un toDto()m茅todo que asigna nuestros Doctorcampos a los DoctorDtocampos.

    Ahora, para mapear una Doctorinstancia a una DoctorDtoinstancia, har铆amos:

    DoctorDto doctorDto = DoctorMapper.INSTANCE.toDto(doctor);
    

    Nota: Es posible que haya notado un DoctorDtoBuilderen la implementaci贸n anterior. Hemos omitido la implementaci贸n por brevedad, ya que los compiladores tienden a ser largos. MapStruct intentar谩 utilizar su constructor si est谩 presente en la clase. Si no, simplemente lo instanciar谩 a trav茅s de la newpalabra clave.

    Si desea leer m谩s sobre el patr贸n de dise帽o de constructores en Java, 隆lo tenemos cubierto!

    Asignaciones de diferentes campos de origen y destino

    A menudo, un modelo y un DTO no tendr谩n los mismos nombres de campo. Puede haber ligeras variaciones debido a que los miembros del equipo asignan sus propias versiones y c贸mo le gustar铆a empaquetar la informaci贸n para el servicio que solicit贸 el DTO.

    MapStruct proporciona soporte para manejar estas situaciones a trav茅s de la @Mappinganotaci贸n.

    Diferentes nombres de propiedad

    Actualicemos la Doctorclase para incluir specialty:

    public class Doctor {
        private int id;
        private String name;
        private String specialty;
    }
    

    Y para el DoctorDto, agreguemos un specializationcampo:

    public class DoctorDto {
        private int id;
        private String name;
        private String specialization;
    }
    

    Ahora, tendremos que informarnos DoctorMapperde esta discrepancia. Lo haremos configurando los indicadores sourcey targetde la @Mappinganotaci贸n con estas dos variantes:

    @Mapper
    public interface DoctorMapper {
        DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
    
        @Mapping(source = "doctor.specialty", target = "specialization")
        DoctorDto toDto(Doctor doctor);
    }
    

    El specialtycampo de la Doctorclase corresponde al specializationcampo de la DoctorDtoclase.

    Despu茅s de compilar el c贸digo, el procesador de anotaciones ha generado esta implementaci贸n:

    public class DoctorMapperImpl implements DoctorMapper {
    @Override
        public DoctorDto toDto(Doctor doctor) {
            if (doctor == null) {
                return null;
            }
    
            DoctorDtoBuilder doctorDto = DoctorDto.builder();
    
            doctorDto.specialization(doctor.getSpecialty());
            doctorDto.id(doctor.getId());
            doctorDto.name(doctor.getName());
    
            return doctorDto.build();
        }
    }
    

    Varias clases de fuentes

    A veces, una sola clase no es suficiente para construir un DTO. A veces, queremos agregar valores de varias clases en un solo DTO para el usuario final. Esto tambi茅n se hace estableciendo las banderas apropiadas en la @Mappinganotaci贸n:

    Creemos otro modelo Education:

    public class Education {
        private String degreeName;
        private String institute;
        private Integer yearOfPassing;
    }
    

    Y agregue un nuevo campo en DoctorDto:

    public class DoctorDto {
        private int id;
        private String name;
        private String degree;
        private String specialization;
    }
    

    Ahora, actualice la DoctorMapperinterfaz:

    @Mapper
    public interface DoctorMapper {
        DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
    
        @Mapping(source = "doctor.specialty", target = "specialization")
        @Mapping(source = "education.degreeName", target = "degree")
        DoctorDto toDto(Doctor doctor, Education education);
    }
    

    Hemos agregado otra @Mappinganotaci贸n en la que hemos establecido el origen como el degreeNamede la Educationclase y el targetcomo el degreecampo de la DoctorDtoclase.

    Si las clases Educationy Doctorcontienen campos con el mismo nombre, tendremos que informar al asignador cu谩l usar o lanzar谩 una excepci贸n. Si ambos modelos contienen un id, tendremos que elegir cu谩l idse asignar谩 a la propiedad DTO.

    Asignaci贸n de entidades secundarias

    En la mayor铆a de los casos, los POJO no contienen solo tipos de datos primitivos. En la mayor铆a de los casos, contendr谩n otras clases. Por ejemplo, a Doctortendr谩 1..npacientes:

    public class Patient {
        private int id;
        private String name;
    }
    

    Y hagamos uno Listde ellos para Doctor:

    public class Doctor {
        private int id;
        private String name;
        private String specialty;
        private List<Patient> patientList;
    }
    

    Dado que los Patientdatos se transferir谩n, tambi茅n crearemos un DTO:

    public class PatientDto {
        private int id;
        private String name;
    }
    

    Y finalmente, actualice el DoctorDtocon uno Listde los reci茅n creados PatientDto:

    public class DoctorDto {
        private int id;
        private String name;
        private String degree;
        private String specialization;
        private List<PatientDto> patientDtoList;
    }
    

    Antes de cambiar algo en DoctorMapper, tendremos que hacer un mapeador que convierta entre las clases Patienty PatientDto:

    @Mapper
    public interface PatientMapper {
        PatientMapper INSTANCE = Mappers.getMapper(PatientMapper.class);
        PatientDto toDto(Patient patient);
    }
    

    Es un mapeador b谩sico que solo mapea un par de tipos de datos primitivos.

    Ahora, actualice nuestro DoctorMapperpara incluir a los pacientes del m茅dico:

    @Mapper(uses = {PatientMapper.class})
    public interface DoctorMapper {
    
        DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
    
        @Mapping(source = "doctor.patientList", target = "patientDtoList")
        @Mapping(source = "doctor.specialty", target = "specialization")
        DoctorDto toDto(Doctor doctor);
    }
    

    Dado que estamos trabajando con otra clase que requiere mapeo, hemos establecido la usesmarca de la @Mapperanotaci贸n. Este @Mapperusa otro @Mapper. Puede poner tantas clases / mapeadores aqu铆 como desee; solo tenemos uno.

    Debido a que agregamos esta bandera, al generar la implementaci贸n del mapeador para la DoctorMapperinterfaz, MapStruct tambi茅n convertir谩 el Patientmodelo en un PatientDto, ya que registramos el PatientMapperpara esta tarea.

    Ahora, compilar la aplicaci贸n dar谩 como resultado una nueva implementaci贸n:

    public class DoctorMapperImpl implements DoctorMapper {
        private final PatientMapper patientMapper = Mappers.getMapper( PatientMapper.class );
    
        @Override
        public DoctorDto toDto(Doctor doctor) {
            if ( doctor == null ) {
                return null;
            }
    
            DoctorDtoBuilder doctorDto = DoctorDto.builder();
    
            doctorDto.patientDtoList( patientListToPatientDtoList(doctor.getPatientList()));
            doctorDto.specialization( doctor.getSpecialty() );
            doctorDto.id( doctor.getId() );
            doctorDto.name( doctor.getName() );
    
            return doctorDto.build();
        }
        
        protected List<PatientDto> patientListToPatientDtoList(List<Patient> list) {
            if ( list == null ) {
                return null;
            }
    
            List<PatientDto> list1 = new ArrayList<PatientDto>( list.size() );
            for ( Patient patient : list ) {
                list1.add( patientMapper.toDto( patient ) );
            }
    
            return list1;
        }
    }
    

    Evidentemente, patientListToPatientDtoList()se ha agregado un nuevo mapeador , adem谩s del toDto()mapeador. Esto se hace sin una definici贸n expl铆cita, simplemente porque agregamos el PatientMapperal DoctorMapper.

    El m茅todo itera sobre una lista de Patientmodelos, los convierte en PatientDtosy los agrega a una lista contenida dentro de un DoctorDtoobjeto.

    Actualizaci贸n de instancias existentes

    A veces, deseamos actualizar un modelo con los 煤ltimos valores de un DTO. Usando la @MappingTargetanotaci贸n en el objeto de destino ( Doctoren nuestro caso), podemos actualizar las instancias existentes.

    Agreguemos un nuevo @Mappinga nuestro DoctorMapperque acepta Doctore DoctorDtoinstancias. La DoctorDtoinstancia ser谩 la fuente de datos, mientras Doctorque ser谩 el destino:

    @Mapper(uses = {PatientMapper.class})
    public interface DoctorMapper {
    
        DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
    
        @Mapping(source = "doctorDto.patientDtoList", target = "patientList")
        @Mapping(source = "doctorDto.specialization", target = "specialty")
        void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor);
    }
    

    Ahora, despu茅s de generar la implementaci贸n nuevamente, tenemos el updateModel()m茅todo:

    public class DoctorMapperImpl implements DoctorMapper {
    
        @Override
        public void updateModel(DoctorDto doctorDto, Doctor doctor) {
            if (doctorDto == null) {
                return;
            }
    
            if (doctor.getPatientList() != null) {
                List<Patient> list = patientDtoListToPatientList(doctorDto.getPatientDtoList());
                if (list != null) {
                    doctor.getPatientList().clear();
                    doctor.getPatientList().addAll(list);
                }
                else {
                    doctor.setPatientList(null);
                }
            }
            else {
                List<Patient> list = patientDtoListToPatientList(doctorDto.getPatientDtoList());
                if (list != null) {
                    doctor.setPatientList(list);
                }
            }
            doctor.setSpecialty(doctorDto.getSpecialization());
            doctor.setId(doctorDto.getId());
            doctor.setName(doctorDto.getName());
        }
    }
    

    Lo que vale la pena se帽alar es que la lista de pacientes tambi茅n se actualiza, ya que es una entidad secundaria del m贸dulo.

    Inyecci贸n de dependencia

    Hasta ahora, hemos estado accediendo a los mapeadores generados a trav茅s del getMapper()m茅todo:

    DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
    

    Sin embargo, si est谩 utilizando Spring , puede actualizar la configuraci贸n de su mapeador e inyectarla como una dependencia normal.

    Actualicemos nuestro DoctorMapperpara trabajar con Spring:

    @Mapper(componentModel = "spring")
    public interface DoctorMapper {}
    

    Agregar (componentModel = "spring")la @Mapperanotaci贸n le dice a MapStruct que al generar la clase de implementaci贸n del asignador, nos gustar铆a que se creara con el soporte de inyecci贸n de dependencia a trav茅s de Spring. Ahora, no es necesario agregar el INSTANCEcampo a la interfaz.

    El generado DoctorMapperImplahora tendr谩 la @Componentanotaci贸n:

    @Component
    public class DoctorMapperImpl implements DoctorMapper {}
    

    Una vez marcado como a @Component, Spring puede elegirlo como un bean y usted es libre de @Autowirehacerlo en otra clase, como un controlador:

    @Controller
    public class DoctorController() {
        @Autowired
        private DoctorMapper doctorMapper;
    }
    

    Si no est谩 utilizando Spring, MapStruct tambi茅n es compatible con Java CDI :

    @Mapper(componentModel = "cdi")
    public interface DoctorMapper {}
    

    Mapeo de enumeraciones

    La asignaci贸n de enumeraciones funciona de la misma manera que la asignaci贸n de campos. MapStruct mapear谩 los que tengan los mismos nombres sin ning煤n problema. Sin embargo, para Enums con diferentes nombres, usaremos la @ValueMappinganotaci贸n. Nuevamente, esto es similar a la @Mappinganotaci贸n con tipos regulares.

    Creemos dos enumeraciones, siendo la primera PaymentType:

    public enum PaymentType {
        CASH,
        CHEQUE,
        CARD_VISA,
        CARD_MASTER,
        CARD_CREDIT
    }
    

    Estas son, digamos, las opciones de pago disponibles en una aplicaci贸n. Y ahora, tengamos una vista m谩s general y limitada de esas opciones:

    public enum PaymentTypeView {
        CASH,
        CHEQUE,
        CARD
    }
    

    Ahora, hagamos una interfaz de mapeador entre estos dos enums:

    @Mapper
    public interface PaymentTypeMapper {
    
        PaymentTypeMapper INSTANCE = Mappers.getMapper(PaymentTypeMapper.class);
    
        @ValueMappings({
                @ValueMapping(source = "CARD_VISA", target = "CARD"),
                @ValueMapping(source = "CARD_MASTER", target = "CARD"),
                @ValueMapping(source = "CARD_CREDIT", target = "CARD")
        })
        PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);
    }
    

    Aqu铆, tenemos un general CARDde valor, y m谩s espec铆ficos CARD_VISA, CARD_MASTERy CARD_CREDITvalores. Hay una discrepancia con el n煤mero de valores: PaymentTypetiene 6 valores, mientras que PaymentTypeViewsolo tiene 3.

    Para hacer un puente entre estos, podemos usar la @ValueMappingsanotaci贸n, que acepta m煤ltiples @ValueMappinganotaciones. Aqu铆, podemos establecer la fuente para que sea cualquiera de los tres casos espec铆ficos y el destino como el CARDvalor.

    MapStruct manejar谩 estos casos:

    public class PaymentTypeMapperImpl implements PaymentTypeMapper {
    
        @Override
        public PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType) {
            if (paymentType == null) {
                return null;
            }
    
            PaymentTypeView paymentTypeView;
    
            switch (paymentType) {
                case CARD_VISA: paymentTypeView = PaymentTypeView.CARD;
                break;
                case CARD_MASTER: paymentTypeView = PaymentTypeView.CARD;
                break;
                case CARD_CREDIT: paymentTypeView = PaymentTypeView.CARD;
                break;
                case CASH: paymentTypeView = PaymentTypeView.CASH;
                break;
                case CHEQUE: paymentTypeView = PaymentTypeView.CHEQUE;
                break;
                default: throw new IllegalArgumentException( "Unexpected enum constant: " + paymentType );
            }
            return paymentTypeView;
        }
    }
    

    CASHy CHEQUEtienen sus valores correspondientes por defecto, mientras que el CARDvalor espec铆fico se maneja a trav茅s de un switchbucle.

    Sin embargo, este enfoque puede resultar poco pr谩ctico cuando tiene muchos valores que le gustar铆a asignar a uno m谩s general. En lugar de asignar cada uno manualmente, simplemente podemos dejar que MapStruct revise todos los valores restantes disponibles y mapearlos todos a otro.

    Esto se hace a trav茅s de MappingConstants:

    @ValueMapping(source = MappingConstants.ANY_REMAINING, target = "CARD")
    PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);
    

    Aqu铆, una vez realizadas las asignaciones predeterminadas, se asignar谩n todos los valores restantes (que no coincidan) CARD.

    @Override
    public PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType) {
        if ( paymentType == null ) {
            return null;
        }
    
        PaymentTypeView paymentTypeView;
    
        switch ( paymentType ) {
            case CASH: paymentTypeView = PaymentTypeView.CASH;
            break;
            case CHEQUE: paymentTypeView = PaymentTypeView.CHEQUE;
            break;
            default: paymentTypeView = PaymentTypeView.CARD;
        }
        return paymentTypeView;
    }
    

    Otra opci贸n ser铆a utilizar ANY_UNMAPPED:

    @ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "CARD")
    PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);
    

    En este caso, en lugar de mapear primero los valores predeterminados, y luego mapear los restantes a un solo objetivo, MapStruct simplemente mapear谩 todos los valores no mapeados al objetivo.

    Mapeo de tipos de datos

    MapStruct admite la conversi贸n de tipos de datos entre propiedades sourcey target. Tambi茅n proporciona conversi贸n autom谩tica de tipos entre tipos primitivos y sus envoltorios correspondientes.

    La conversi贸n autom谩tica de tipos se aplica a:

    • Conversi贸n entre tipos primitivos y sus respectivos tipos de envoltura. Por ejemplo, la conversi贸n entre inty Integer, floaty Float, longy Long, booleany Booleanetc.
    • Conversi贸n entre cualquier tipo primitivo y cualquier tipo de envoltorio. Por ejemplo, entre inty long, bytey Integeretc.
    • Conversi贸n entre todos los tipos primitivos y envoltorios y String. Por ejemplo, la conversi贸n entre booleany String, Integery String, floaty Stringetc.

    Por lo tanto, durante la generaci贸n del c贸digo del asignador, si la conversi贸n de tipo entre el campo de origen y el de destino se encuentra en cualquiera de los escenarios anteriores, MapStrcut manejar谩 la conversi贸n de tipo en s铆.

    Actualicemos nuestro PatientDtopara incluir un campo para almacenar dateofBirth:

    public class PatientDto {
        private int id;
        private String name;
        private LocalDate dateOfBirth;
    }
    

    Por otro lado, digamos que nuestro Patientobjeto tiene un dateOfBirthtipo de String:

    public class Patient {
        private int id;
        private String name;
        private String dateOfBirth;
    }
    

    Ahora, sigamos adelante y creemos un mapeador entre estos dos:

    @Mapper
    public interface PatientMapper {
    
        @Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
        Patient toModel(PatientDto patientDto);
    }
    

    Al convertir entre fechas, tambi茅n podemos usar la dateFormatbandera para establecer el especificador de formato. La implementaci贸n generada se ver谩 as铆:

    public class PatientMapperImpl implements PatientMapper {
    
        @Override
        public Patient toModel(PatientDto patientDto) {
            if (patientDto == null) {
                return null;
            }
    
            PatientBuilder patient = Patient.builder();
    
            if (patientDto.getDateOfBirth() != null) {
                patient.dateOfBirth(DateTimeFormatter.ofPattern("dd/MMM/yyyy")
                                    .format(patientDto.getDateOfBirth()));
            }
            patient.id(patientDto.getId());
            patient.name(patientDto.getName());
    
            return patient.build();
        }
    }
    

    Tenga en cuenta que MapStruct ha utilizado el patr贸n proporcionado por la dateFormatbandera. Si no especificamos el formato, se habr铆a configurado en el formato predeterminado de a LocalDate:

    if (patientDto.getDateOfBirth() != null) {
        patient.dateOfBirth(DateTimeFormatter.ISO_LOCAL_DATE
                            .format(patientDto.getDateOfBirth()));
    }
    

    Agregar m茅todos personalizados

    Hasta ahora, hemos estado agregando un m茅todo de marcador de posici贸n que queremos que MapStruct lo implemente. Lo que tambi茅n podemos hacer es agregar un defaultm茅todo personalizado a la interfaz. Al agregar un defaultm茅todo, tambi茅n podemos agregar la implementaci贸n directamente. Podremos acceder a 茅l a trav茅s de la instancia sin problema.

    Para esto, hagamos a DoctorPatientSummary, que contiene un resumen entre ay Doctoruna lista de sus Patients:

    public class DoctorPatientSummary {
        private int doctorId;
        private int patientCount;
        private String doctorName;
        private String specialization;
        private String institute;
        private List<Integer> patientIds;
    }
    

    Ahora, en nuestro DoctorMapper, agregaremos un defaultm茅todo que, en lugar de mapear a Doctora a DoctorDto, convierte los objetos Doctory Educationen a DoctorPatientSummary:

    @Mapper
    public interface DoctorMapper {
    
        default DoctorPatientSummary toDoctorPatientSummary(Doctor doctor, Education education) {
    
            return DoctorPatientSummary.builder()
                    .doctorId(doctor.getId())
                    .doctorName(doctor.getName())
                    .patientCount(doctor.getPatientList().size())
    				.patientIds(doctor.getPatientList()
                	        .stream()
                            .map(Patient::getId)
                	        .collect(Collectors.toList()))
                	.institute(education.getInstitute())
                    .specialization(education.getDegreeName())
                    .build();
        }
    }
    

    Este objeto se crea a partir de los objetos Doctory Educationutilizando el patr贸n Builder Design.

    Esta implementaci贸n estar谩 disponible para su uso despu茅s de que MapStruct genere la clase de implementaci贸n del asignador. Puede acceder a 茅l del mismo modo que acceder铆a a cualquier otro m茅todo de mapeador:

    DoctorPatientSummary summary = doctorMapper.toDoctorPatientSummary(dotor, education);
    

    Creaci贸n de mapeadores personalizados

    Hasta ahora, hemos estado usando interfaces para crear planos para mapeadores. Tambi茅n podemos hacer planos con abstractclases, anotadas con la @Mapperanotaci贸n. MapStruct crear谩 una implementaci贸n para esta clase, similar a la creaci贸n de una implementaci贸n de interfaz.

    Reescribamos el ejemplo anterior, aunque esta vez lo convertiremos en una abstractclase:

    @Mapper
    public abstract class DoctorCustomMapper {
        public DoctorPatientSummary toDoctorPatientSummary(Doctor doctor, Education education) {
    
            return DoctorPatientSummary.builder()
                    .doctorId(doctor.getId())
                    .doctorName(doctor.getName())
                    .patientCount(doctor.getPatientList().size())
                    .patientIds(doctor.getPatientList()
                            .stream()
                            .map(Patient::getId)
                            .collect(Collectors.toList()))
                    .institute(education.getInstitute())
                    .specialization(education.getDegreeName())
                    .build();
        }
    }
    

    Puede usar esta implementaci贸n de la misma manera que usar铆a una implementaci贸n de interfaz. El uso de abstractclases nos brinda m谩s control y opciones al crear implementaciones personalizadas debido a menos limitaciones. Otra ventaja es la posibilidad de a帽adir @BeforeMappingy @AfterMappingm茅todos.

    @BeforeMapping y @AfterMapping

    Para un control y personalizaci贸n adicionales, podemos definir @BeforeMappingy @AfterMappingm茅todos. Obviamente, estos se ejecutan antes y despu茅s de cada mapeo. Es decir, estos m茅todos se agregar谩n y ejecutar谩n antes y despu茅s del mapeo real entre dos objetos dentro de la implementaci贸n.

    Agreguemos estos m茅todos a nuestro DoctorCustomMapper:

    @Mapper(uses = {PatientMapper.class}, componentModel = "spring")
    public abstract class DoctorCustomMapper {
    
        @BeforeMapping
        protected void validate(Doctor doctor) {
            if(doctor.getPatientList() == null){
                doctor.setPatientList(new ArrayList<>());
            }
        }
    
        @AfterMapping
        protected void updateResult(@MappingTarget DoctorDto doctorDto) {
            doctorDto.setName(doctorDto.getName().toUpperCase());
            doctorDto.setDegree(doctorDto.getDegree().toUpperCase());
            doctorDto.setSpecialization(doctorDto.getSpecialization().toUpperCase());
        }
    
        @Mapping(source = "doctor.patientList", target = "patientDtoList")
        @Mapping(source = "doctor.specialty", target = "specialization")
        public abstract DoctorDto toDoctorDto(Doctor doctor);
    }
    

    Ahora, generemos un mapeador basado en esta clase:

    @Component
    public class DoctorCustomMapperImpl extends DoctorCustomMapper {
        
        @Autowired
        private PatientMapper patientMapper;
        
        @Override
        public DoctorDto toDoctorDto(Doctor doctor) {
            validate(doctor);
    
            if (doctor == null) {
                return null;
            }
    
            DoctorDto doctorDto = new DoctorDto();
    
            doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor
                .getPatientList()));
            doctorDto.setSpecialization(doctor.getSpecialty());
            doctorDto.setId(doctor.getId());
            doctorDto.setName(doctor.getName());
    
            updateResult(doctorDto);
    
            return doctorDto;
        }
    }
    

    El validate()m茅todo se ejecuta antes de DoctorDtoque se cree una instancia del objeto y el updateResult()m茅todo se ejecuta una vez finalizada la asignaci贸n.

    Agregar valores predeterminados

    Un par de indicadores 煤tiles que puede usar con la @Mappinganotaci贸n son las constantes y los valores predeterminados. constantSiempre se utilizar谩 un valor, independientemente del sourcevalor de. Se defaultutilizar谩 un valor si el sourcevalor es null.

    Actualicemos nuestro DoctorMappercon un constanty default:

    @Mapper(uses = {PatientMapper.class}, componentModel = "spring")
    public interface DoctorMapper {
        @Mapping(target = "id", constant = "-1")
        @Mapping(source = "doctor.patientList", target = "patientDtoList")
        @Mapping(source = "doctor.specialty", target = "specialization", defaultValue = "Information Not Available")
        DoctorDto toDto(Doctor doctor);
    }
    

    Si la especialidad no est谩 disponible, asignaremos la Information Not Availablecadena en su lugar. Adem谩s, hemos codificado el idto be -1.

    Generemos el mapeador:

    @Component
    public class DoctorMapperImpl implements DoctorMapper {
    
        @Autowired
        private PatientMapper patientMapper;
        
        @Override
        public DoctorDto toDto(Doctor doctor) {
            if (doctor == null) {
                return null;
            }
    
            DoctorDto doctorDto = new DoctorDto();
    
            if (doctor.getSpecialty() != null) {
                doctorDto.setSpecialization(doctor.getSpecialty());
            }
            else {
                doctorDto.setSpecialization("Information Not Available");
            }
            doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor.getPatientList()));
            doctorDto.setName(doctor.getName());
    
            doctorDto.setId(-1);
    
            return doctorDto;
        }
    }
    

    Si doctor.getSpecialty()regresa null, establecemos la especializaci贸n en nuestro mensaje predeterminado. El idse establece independientemente, ya que es un constant.

    Agregar expresiones de Java

    MapStruct va tan lejos como para permitirle ingresar completamente expresiones Java como indicadores para la @Mappinganotaci贸n. Puede establecer a defaultExpression(si el sourcevalor es null) o expressioncu谩l es constante.

    Agreguemos un externalIdque ser谩 un Stringy un appointmentque ser谩 de LocalDateTimetipo a nuestro Doctory DoctorDto.

    Nuestro Doctormodelo se ver谩 as铆:

    public class Doctor {
    
        private int id;
        private String name;
        private String externalId;
        private String specialty;
        private LocalDateTime availability;
        private List<Patient> patientList;
    }
    

    Y DoctorDtose ver谩 as铆:

    public class DoctorDto {
    
        private int id;
        private String name;
        private String externalId;
        private String specialization;
        private LocalDateTime availability;
        private List<PatientDto> patientDtoList;
    }
    

    Y ahora, actualice nuestro DoctorMapper:

    @Mapper(uses = {PatientMapper.class}, componentModel = "spring", imports = {LocalDateTime.class, UUID.class})
    public interface DoctorMapper {
    
        @Mapping(target = "externalId", expression = "java(UUID.randomUUID().toString())")
        @Mapping(source = "doctor.availability", target = "availability", defaultExpression = "java(LocalDateTime.now())")
        @Mapping(source = "doctor.patientList", target = "patientDtoList")
        @Mapping(source = "doctor.specialty", target = "specialization")
        DoctorDto toDtoWithExpression(Doctor doctor);
    }
    

    Aqu铆, hemos asignado el valor de java(UUID.randomUUID().toString())a externalId, mientras que hemos establecido condicionalmente la disponibilidad a un nuevo LocalDateTime, si availabilityno est谩 presente.

    Dado que las expresiones son solo Strings, tenemos que especificar las clases utilizadas en las expresiones. Este no es un c贸digo que se est谩 evaluando, es un valor de texto literal. Por lo tanto, hemos agregado imports = {LocalDateTime.class, UUID.class}a la @Mapperanotaci贸n.

    El mapeador generado se ver谩 as铆:

    @Component
    public class DoctorMapperImpl implements DoctorMapper {
    
        @Autowired
        private PatientMapper patientMapper;
        
        @Override
        public DoctorDto toDtoWithExpression(Doctor doctor) {
            if (doctor == null) {
                return null;
            }
    
            DoctorDto doctorDto = new DoctorDto();
    
            doctorDto.setSpecialization(doctor.getSpecialty());
            if (doctor.getAvailability() != null) {
                doctorDto.setAvailability(doctor.getAvailability());
            }
            else {
                doctorDto.setAvailability(LocalDateTime.now());
            }
            doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor
                .getPatientList()));
            doctorDto.setId(doctor.getId());
            doctorDto.setName(doctor.getName());
    
            doctorDto.setExternalId(UUID.randomUUID().toString());
    
            return doctorDto;
        }
    }
    

    El externalIdest谩 configurado para:

    doctorDto.setExternalId(UUID.randomUUID().toString());
    

    Mientras que, si availabilityes as铆 null, est谩 configurado para:

    doctorDto.setAvailability(LocalDateTime.now());
    

    Manejo de excepciones durante el mapeo

    El manejo de excepciones es inevitable. Las aplicaciones incurren en estados excepcionales todo el tiempo. MapStruct proporciona soporte para incluir el manejo de excepciones sin problemas, lo que simplifica mucho su trabajo como desarrollador.

    Consideremos un escenario en el que queremos validar nuestro Doctormodelo mientras lo asignamos DoctorDto. Hagamos una Validatorclase separada para esto:

    public class Validator {
        public int validateId(int id) throws ValidationException {
            if(id == -1){
                throw new ValidationException("Invalid value in ID");
            }
            return id;
        }
    }
    

    Ahora, queremos actualizar nuestro DoctorMapperpara usar la Validatorclase, sin que tengamos que especificar la implementaci贸n. Como de costumbre, agregaremos las clases a la lista de clases utilizadas por @Mapper, y todo lo que tenemos que hacer es decirle a MapStruct que nuestro toDto()m茅todo throws ValidationException:

    @Mapper(uses = {PatientMapper.class, Validator.class}, componentModel = "spring")
    public interface DoctorMapper {
    
        @Mapping(source = "doctor.patientList", target = "patientDtoList")
        @Mapping(source = "doctor.specialty", target = "specialization")
        DoctorDto toDto(Doctor doctor) throws ValidationException;
    }
    

    Ahora, generemos una implementaci贸n para este mapeador:

    @Component
    public class DoctorMapperImpl implements DoctorMapper {
    
        @Autowired
        private PatientMapper patientMapper;
        @Autowired
        private Validator validator;
    
        @Override
        public DoctorDto toDto(Doctor doctor) throws ValidationException {
            if (doctor == null) {
                return null;
            }
    
            DoctorDto doctorDto = new DoctorDto();
    
            doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor
                .getPatientList()));
            doctorDto.setSpecialization(doctor.getSpecialty());
            doctorDto.setId(validator.validateId(doctor.getId()));
            doctorDto.setName(doctor.getName());
            doctorDto.setExternalId(doctor.getExternalId());
            doctorDto.setAvailability(doctor.getAvailability());
    
            return doctorDto;
        }
    }
    

    MapStruct ha establecido autom谩ticamente el ID de doctorDtocon el resultado de la Validatorinstancia. Tambi茅n agreg贸 una throwscl谩usula para el m茅todo.

    Configuraciones de mapeo

    MapStruct proporciona una configuraci贸n muy 煤til para escribir m茅todos de mapeador. La mayor铆a de las veces, las configuraciones de mapeo que especificamos para un m茅todo de mapeador se replican al agregar otro m茅todo de mapeador para tipos similares.

    En lugar de configurarlos manualmente, podemos configurar tipos similares para tener los mismos m茅todos de mapeo o similares.

    Heredar configuraci贸n

    Revisemos el escenario en Actualizaci贸n de instancias existentes, donde creamos un asignador para actualizar los valores de un Doctormodelo existente a partir de un DoctorDtoobjeto:

    @Mapper(uses = {PatientMapper.class})
    public interface DoctorMapper {
    
        DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
    
        @Mapping(source = "doctorDto.patientDtoList", target = "patientList")
        @Mapping(source = "doctorDto.specialization", target = "specialty")
        void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor);
    }
    

    Digamos que tenemos otro mapeador que genera un a Doctorpartir de un DoctorDto:

    @Mapper(uses = {PatientMapper.class, Validator.class})
    public interface DoctorMapper {
    
        @Mapping(source = "doctorDto.patientDtoList", target = "patientList")
        @Mapping(source = "doctorDto.specialization", target = "specialty")
        Doctor toModel(DoctorDto doctorDto);
    }
    

    Ambos m茅todos de mapeador utilizan la misma configuraci贸n. La sourcesy la targets son iguales. En lugar de repetir las configuraciones para ambos m茅todos de mapeadores, podemos usar la @InheritConfigurationanotaci贸n.

    Al anotar un m茅todo con la @InheritConfigurationanotaci贸n, MapStruct buscar谩 otro m茅todo ya configurado cuya configuraci贸n tambi茅n se pueda aplicar a este. Por lo general, esta anotaci贸n se usa para m茅todos de actualizaci贸n despu茅s de un m茅todo de mapeo, tal como lo estamos usando:

    @Mapper(uses = {PatientMapper.class, Validator.class}, componentModel = "spring")
    public interface DoctorMapper {
    
        @Mapping(source = "doctorDto.specialization", target = "specialty")
        @Mapping(source = "doctorDto.patientDtoList", target = "patientList")
        Doctor toModel(DoctorDto doctorDto);
    
        @InheritConfiguration
        void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor);
    }
    

    Heredar configuraci贸n inversa

    Otro escenario similar es escribir funciones de mapeador para mapear Model a DTO y DTO a Model , como en el siguiente c贸digo donde tenemos que especificar la misma asignaci贸n de destino de origen en ambas funciones:

    Tus configuraciones no siempre ser谩n las mismas. Por ejemplo, pueden ser inversas. Asignar un modelo a un DTO y un DTO a un modelo: utiliza los mismos campos, pero a la inversa. As铆 es como se ve normalmente:

    @Mapper(componentModel = "spring")
    public interface PatientMapper {
    
        @Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
        Patient toModel(PatientDto patientDto);
    
        @Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
        PatientDto toDto(Patient patient);
    }
    

    En lugar de escribir esto dos veces, podemos usar la @InheritInverseConfigurationanotaci贸n en el segundo m茅todo:

    @Mapper(componentModel = "spring")
    public interface PatientMapper {
    
        @Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
        Patient toModel(PatientDto patientDto);
    
        @InheritInverseConfiguration
        PatientDto toDto(Patient patient);
    }
    

    El c贸digo generado de ambas implementaciones del mapeador ser谩 el mismo.

    Conclusi贸n

    En este art铆culo exploramos MapStruct, una biblioteca para crear clases de mapeadores, comenzando desde mapeos de nivel b谩sico hasta m茅todos personalizados y mapeadores personalizados. Tambi茅n analizamos diferentes opciones proporcionadas por MapStruct, incluida la inyecci贸n de dependencias, las asignaciones de tipos de datos, las asignaciones de enumeraci贸n y el uso de expresiones.

    MapStruct proporciona un potente complemento de integraci贸n para reducir la cantidad de c贸digo que tiene que escribir un usuario y hace que el proceso de creaci贸n de mapeadores sea f谩cil y r谩pido.

    El c贸digo fuente del c贸digo de muestra se puede encontrar aqu铆 .

    .

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *