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 *