Surprisingly enough I have made it to this point in life without ever having a Parent/Child relationship. I won't dwell on how very many examples I've looked at other than to say I've looked at MANY. This guy seems among the best. Only problem is every example I can find is either "abbreviated" (only the parts the author thought were interesting, or so old they don't compile at all, or don't use JPA, etc etc.
My very simple case is that I have an entity representing a Rest endpoint. The child table will have a row for each http header needed for the call. (There's more to it of course but this is the most basic example).
The target environment is current versions of everything e.g. Spring Boot 2.1.3.RELEASE etc.
After much messing around I got the DB rows created, although "I" think there should be a better way. However I cannot get the find to instantiate the child collection.
PARENT MODEL (some columns removed just to make the post shorter)
@Entity
@Table(name = "EXT_PROVIDER")
public class ExtProvider extends AuditModel {
private String serviceName;
private String serviceType;
@Id
@Column(name = "SERVICE_NAME", nullable = false, length = 30)
public String getServiceName() {
return serviceName;
}
public void setServiceName(String serviceName) {
this.serviceName = serviceName;
}
@Basic
@Column(name = "SERVICE_TYPE", nullable = false, length = 50)
public String getServiceType() {
return serviceType;
}
public void setServiceType(String serviceType) {
this.serviceType = serviceType;
}
@OneToMany(mappedBy = "extProvider", cascade = CascadeType.ALL, orphanRemoval = true)
public List<ExtProviderHeader> headers = new ArrayList<>();
public void addExtProviderHeader(ExtProviderHeader header) {
headers.add(header);
header.setExtProvider(this);
}
public void removeExtProviderHeader(ExtProviderHeader header) {
headers.remove(header);
header.setExtProvider(null);
}
public void setExtProviderHeader(List<ExtProviderHeader> headers) {
this.headers = headers;
}
public void setHeaders(List<ExtProviderHeader> headers) {
this.headers = headers;
}
If this is uncommented hibernate fails with hibernate.MappingException: Could not determine type for: java.util.List, at table: ext_provider, for columns: [org.hibernate.mapping.Column(headers)]
But if you can't do this how is the @Service supposed to get the data even if it does ever load
//public List<ExtProviderHeader> getHeaders() {
// return headers;
//}
CHILD MODEL
@Entity
@Table(name = "EXT_PROVIDER_HEADER")
public class ExtProviderHeader {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "extProviderSeq")
@SequenceGenerator(name = "extProviderSeq", sequenceName = "EXT_PROVIDER_SEQ", allocationSize = 5)
@Column(name = "DATA_ID", nullable = false, precision = 0)
private Long dataId;
public Long getDataId() {
return dataId;
}
public void setDataId(Long dataId) {
this.dataId = dataId;
}
@Basic
@Column(name = "HEADER_KEY", nullable = false, length = 30)
private String headerKey;
public String getHeaderKey() {
return headerKey;
}
public void setHeaderKey(String headerKey) {
this.headerKey = headerKey;
}
@Basic
@Column(name = "HEADER_VALUE", nullable = false, length = 50)
private String headerValue;
public String getHeaderValue() {
return headerValue;
}
public void setHeaderValue(String headerValue) {
this.headerValue = headerValue;
}
@Basic
@Column(name = "SERVICE_NAME", nullable = false, length = 30, insertable = true, updatable = true)
private String serviceName;
public String getServiceName() { return serviceName; }
public void setServiceName(String serviceName) { this.serviceName = serviceName; }
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "SERVICE_NAME", insertable = false, updatable = false)
private ExtProvider extProvider;
public ExtProvider getExtProvider() { return extProvider; }
public void setExtProvider(ExtProvider extProvider) { this.extProvider = extProvider; }
REPOSITORIES
@Repository
public interface ExtProviderRepository extends JpaRepository<ExtProvider, String> {
}
@Repository
public interface ExtProviderHeaderRepository extends JpaRepository<ExtProviderHeader, Long> {
ExtProviderHeader findByDataId(Long primaryKey);
}
JUNIT to Create rows
I think there is no way around the multiple saves because the primary key for the child rows is a SEQUENCE and there is a FOREIGN KEY relationship to the Parent. So while I don't like this it seems stuck.
Different advice appreciated
@Test
@DisplayName("Save Provder and Headers")
public void saveProvider() {
ExtProvider extProvider = new ExtProvider();
extProvider.setServiceName("MS6");
extProvider.setServiceType("MESSAGING");
extProvider = extProviderRepository.save(extProvider);
ExtProviderHeader extProviderHeader = new ExtProviderHeader();
extProviderHeader.setHeaderKey("Authorization: OAuth");
extProviderHeader.setHeaderValue("xxxx-xxxxxxxx");
extProviderHeader.setServiceName(extProvider.getServiceName());
extProviderHeader = extProviderHeaderRepository.save(extProviderHeader);
extProvider.addExtProviderHeader(extProviderHeader);
extProviderHeader = new ExtProviderHeader();
extProviderHeader.setHeaderKey("Content-Type");
extProviderHeader.setHeaderValue("application/json");
extProviderHeader.setServiceName(extProvider.getServiceName());
extProviderHeader = extProviderHeaderRepository.save(extProviderHeader);
extProvider.addExtProviderHeader(extProviderHeader);
ExtProvider save = extProviderRepository.save(extProvider);
logger.info("Provider Ceated");
}
JUNIT to instantiate object back into memory
This is where the headers collection comes back empty.
@Test
public void createProvider() {
Optional<ExtProvider> optional = extProviderRepository.findById("MS6");
ExtProvider extProvider = optional.get();
logger.info("PROVIDER:{}", extProvider);
List<ExtProviderHeader> headers = extProvider.headers;
for(ExtProviderHeader header : headers) {
logger.info("Header is:{}", header);
}
Observations:
- Obviously not having a getter for headers violates OO. But if I uncomment the getter hibernate fails to initialize. I am missing some important point here.
- Some examples say to replace the LIST with a SET or even COLLECTION. That doesn't matter any at all.
- Most examples do NOT have the Model actually instantiating the collection. Although the link I provided at the top of the post does. This guy is a main contributor to the hibernate project so I want to trust him. This would be easier if his code worked!
- Regardless if the Model does not allocate the ArrayList then the JUNIT gets an NPE when it tries to iterate over the collection. Of course all this really tells us is that Hibernate didn't even try to set this collection. I would expect Hibernate to set an empty list there if this was working at all.
EDIT: I have found that I can get the Headers by making this call:
extProvider.setHeaders(extProviderHeaderRepository.findByExtProvider(extProvider));
This is slightly cool I guess. I had expected when I "referenced" the collection hibernate would populate it - that's how I read FetchType.LAZY is supposed to work. Of course changing to FetchType.Eager didn't do anything different.
I also figured out if I don't use "get" in the name of the method i.e change getHeaders to headersList Hibernate will quit trying to map the collection to a database column.
So with all this it does more or less work. But it is all completely manual. I see no real advantage over having these as separate objects and manually managing them myself.
Maybe I'm still missing something. OR maybe this is all just one abstraction too far to really perform as advertised. I'll continue to poke at it.