简体   繁体   中英

How to map Collections in Mapstruct when generic type has Inheritance structure?

I've the following bean structure in my project.

 public class Account{
       // properties
       // setters
       // getters
    }

   public class AccountType1 extends Acccount{
       // properties
       // setters
       // getters
   }

    public class AccountType3 extends Acccount{
       // properties
       // setters
       // getters
   }

   public class CustomerProfile {
       Customer customer;
       List<Account> accounts;
       List<Service> services;

   }

I've similar structure for Customer and services. One parent and multiple implementation. My application is a middle ware application. I don't know what kind of run time objects our app gets from other webservice calls(Bean model is same across application). List can contain any implementation. It can either be Account or AccountType1 or AccountType2. Same is the case with Service. Parent will have common fields and each implementation will have specific fields. We will have a new flow for each new client ie, consumer. Also field requirement is different. So we need to have separate CustomerProfile and corresponding Account and Service mappers. Now for client1, they may need generic Account or AccountType1 or AccountType2 or AccountTypeN or all of them. So code should be generic like whatever type of classes I give {AccountType1.class, AccounTypeN.class} in config, it should map those objects only from the list. Since AccountType1 extends Account, It should also take care of parent class fields. I'm currently doing this following way.

    @Mapper(config = GlobalConfig.class)
    public interface CustomerProfileMapper{

    @Mappings({
        @Mapping( target = "customer", source = "customer"),
        @Mapping( target = "accounts", source = "accounts"),
        @Mapping( target = "services", source = "services")
    })
    CustomerProfile mapCustomerProfile(CustomerProfile customerProfile);

    @IterableMapping(qualifiedByName = "mapAccount")
    List<Account> mapAccounts(List<Account> accounts);

    @Named("mapAccount")
    default Account mapAccount (Account account){

       if(account instanceof AccountType1){
            mapAccountType1((AccountType1)account);
        }
       else if(account instanceof AccountType2){
            mapAccountType2((AccountType2)account);
        }
       else {
            mapBaseAccount(account);
        }
    }

   @Mappings{...}
   AccountType1 mapAccountType1(AccountType1  account);

   @Mappings{...}
   AccountType2 mapAccountType2(AccountType2  account);
   }

   @Mappings{...}
   Account mapBaseAccount(Account  account);

}

But This code will be redundant as I've to write for each flow for different CustomerProfileMappers. I want code to be generic and can used as a configuration. Re-usability is the concern here. How to address this problem? Basically I want to do something like below.

@IterableMapping( mappingClasses= {AccountType1.class, AccountType2.class, Account.class})
    List<Account> mapAccounts(List<Account> accounts);
     @Mappings{...}
       AccountType1 mapAccountType1(AccountType1  account);

       @Mappings{...}
       AccountType2 mapAccountType2(AccountType2  account);
       }

       @Mappings{...}
       Account mapBaseAccount(Account  account);

So mapStruct should generate code like how I handled this currently. It should generate the mapping method to handle all the specified classes defined in mappingClasses property. It should also look for individual class specific mapping methods in the mappers. If found call them or else generate a mapping method. This is required because I have similar thing with Customer and Service. I don't want too much hand written code in mappers and We have tens of CustomerProfileMappers different for each flow. And they keep increasing with each release. I've gone through the complete technical documentation of MapStruct. But i couldn't find a way to do this. Or this could be a new FR?

You could try to externalise the switch on account.

@Mapper(config = GlobalConfig.class)
public interface CustomerProfileMapper{

    @Mappings({
        @Mapping( target = "customer", source = "customer"),
        @Mapping( target = "accounts", source = "accounts"),
        @Mapping( target = "services", source = "services")
    })
    CustomerProfile mapCustomerProfile(CustomerProfile customerProfile, @Context MyMappingContext ctx);

   @Mappings{...}
   AccountType1 mapAccountType1(AccountType1  account);

   @Mappings{...}
   AccountType2 mapAccountType2(AccountType2  account);
   }

   @Mappings{...}
   Account mapBaseAccount(Account  account);
}

public class MyMappingContext {

    // or whatever component model you prefer
    CustomerProfileMapper mapper = CustomerProfileMapper.INSTANCE;

    @AfterMaping
    public void mapAccount (CustomerProfile source, @MappingTarget CustomerProfile target){

       for ( Account account : source.getAccounts() ){ 

       if(account instanceof AccountType1){
            mapper.mapAccountType1((AccountType1)account);
        }
       else if(account instanceof AccountType2){
            mapper.mapAccountType2((AccountType2)account);
        }
       else {
            mapper.mapBaseAccount(account);
        }
    }
   }
}

You could even do a call-back from this context to the mapper. If you want you could express the generic behaviour of the mapper itself as a generic mapper. So define the signatures in a CommonCustomerProfileMapper and let CustomerProfileMapper1 inherit from that one and define the mappings.

Wrt to a new feature in MapStruct: like said: I'm not sure how much common interest there is for such a feature, but you can always issue a feature request.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM