I'm trying to code my first webapp using Spring Boot. I haven't had much time to dive deep into ORMs, JPA and Hibernate, so it's hard for me to implement application layers with transfer objects. So I have this entities:
@Data
@Entity
public class Dog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "owner_id")
private Owner owner;
}
@Entity
@Data
public class Owner {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "owner")
private Set<Dog> dogs = new HashSet<>();
}
What I'm trying to do is to pass a name for new Dog from controller to DogService and assign an owner ID to it with DTO that looks like this:
@Data
public class CreateDogDTO {
private String name;
private Long ownerId;
}
In my DogService I'm fetching for OwnerDTO with OwnerService
@Data
public class OwnerDTO {
private String firstName;
private String lastName;
@JsonProperty("owner_url")
private String ownerURL;
private Set<GetDogDTO> dogs;
}
@Data
public class GetDogDTO {
private Long id;
private String name;
private OwnerDTO owner;
}
And when I'm creating a new Dog, I get a Hibernate Error:
org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing
I understand that there might be some circular definition with OwnerDTO having set<GetDogDTO> that itself have again OwnerDTO. So here's my first question: How should I construct DTOs to get maximum amount of data to use on the front of my application? Should I create simple OwnerDTO and DogDTO without any relation just to simplify things, or is it possible to force hibernate to create such object for me. I'm using MapStruct for mapping:
@Mapper
public interface DogMapper {
DogMapper INSTANCE = Mappers.getMapper(DogMapper.class);
GetDogDTO dogToDogDTO(Dog dog);
Dog dogDTOToDog(GetDogDTO getDogDTO);
@Mapping(source = "ownerId", target = "owner.id")
Dog saveDogDTOToDog(CreateDogDTO createDogDTO);
}
Is there clear design for handling relations in such examples. I mean it's pretty common stuff but whenever I'm looking at repos with such applications I can't understand how they handle this situation when Repository returns Entity object but I have to deal with DTOs in service layer and Repository doesn't recognize object because it was mapped to DTO and I can't use this information to create a Dog.
To state my question clear. What should I do in Service Layer designed for handling Dog. Should I inject OwnerRepository to this class, get the desired Entity and be done with it, or should I overengineer this, inject OwnerService class and try to create new Dog with data only acquired from OwnerDTO - is there a way to map this object so I don't get Hibernate error.
//Should take CreateDogDTO with ownerID and dog's name.
@Override
public GetDogDTO createNewDog(String dogName) {
Dog dogToCreate = new Dog();
dogToCreate.setName(dogName);
//TODO: Owner must come from session or be present as a parameter in URL
ownerService.getAllOwners().stream().findFirst().ifPresent(owner -> dogToCreate.setOwner(ownerMapper.ownerDTOToOwner(owner)));
Dog createdDog = dogRepository.save(dogToCreate);
System.out.println(createdDog);
return dogMapper.dogToDogDTO(createdDog);
}
@Mapper
public interface OwnerMapper {
OwnerMapper INSTANCE = Mappers.getMapper(OwnerMapper.class);
OwnerDTO ownerToOwnerDTO(Owner owner);
Owner ownerDTOToOwner(OwnerDTO ownerDTO);
}