Sami Asmar
Sami Asmar

Reputation: 131

How to Map an object contains other object with @oneToMany

Given the Source class as defined below:

class Person{
    private String name;
    private int age;
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "peroson")
    List<Phone> phones
    // getters and setters
}

and the Phone class as defined below:

class Phone{
        private Long id;
        private String phoneNumber;
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "person")
        private Person person;
        // getters and setters
    }

and DTO objects:

class PersonDTO{
        private String name;
        private int age;
        List<PhoneDTO> phones
        // getters and setters
    }

class PhoneDTO{
            private Long id;
            private String phoneNumber;
            private PersonDTO person;
            // getters and setters
        }

I have an interface :

@Mapper(uses={PersonMapper.class})
interface PhoneMapper{
    PhoneDTO toDto(Phone source);
    Phone toEntity(PhoneDTO source);
}

 @Mapper(uses={PhoneMapper.class})
    interface PersonMapper{
        PersonDTO toDto(Person source);
        Person toEntity(PersonDTO source);
    }

when I use PeronMapper or PhoneMapper there is a recursive call, because PersonMapper use PhoneMapper and PersonMapper use PhoneMapper.

how to solve that?

Upvotes: 1

Views: 2500

Answers (2)

Prabu M
Prabu M

Reputation: 180

Sometimes the OneToMany child class reference should reflect the same name in the parent Dto. Example

class PersonDTO{
        private String name;
        private int age;
        List<PhoneDTO> phones

    }

this phones name should match exactly in the entity name. And you needs to add Eagar loading as fetch type

@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
@Fetch(value=FetchMode.SELECT)
List<Phone> phones

Upvotes: 0

Filip
Filip

Reputation: 21393

There are numerous ways to avoid the recursion.

First option:

Avoid by explicitly ignoring in one of your mappers

Second option:

Use the @Context and write your own memoization. There is an example mapstruct-mapping-with-cycles in the mapstruct examples repo.

The idea is to have a @Context

public class CycleAvoidingMappingContext {
    private Map<Object, Object> knownInstances = new IdentityHashMap<Object, Object>();

    @BeforeMapping
    public <T> T getMappedInstance(Object source, @TargetType Class<T> targetType) {
        return (T) knownInstances.get( source );
    }

    @BeforeMapping
    public void storeMappedInstance(Object source, @MappingTarget Object target) {
        knownInstances.put( source, target );
    }
}

And then pass in an instance of this context to your mappers:

@Mapper(uses={PersonMapper.class})
interface PhoneMapper{
    PhoneDTO toDto(Phone source, @Context CycleAvoidingContext context);
    Phone toEntity(PhoneDTO source, @Context CycleAvoidingContext context);
}

@Mapper(uses={PhoneMapper.class})
interface PersonMapper{
    PersonDTO toDto(Person source, @Context CycleAvoidingContext context);
    Person toEntity(PersonDTO source, @Context CycleAvoidingContext context);
}

Of course the CycleAvoidingMappingContext can have more specified sources, instead of just Object. You can have one for Person and PersonDTO only for example. It is up to you how you want to implement it.

Edit:

When you have cyclic dependency between Mappers then the only solution is to use a dependency injection framework. The default MapStruct Mappers factory does not support cycles.

Upvotes: 1

Related Questions