I'm moving away from the Play Framework to Spring Boot and I've run into a couple of issues that I'm having a hard time understanding.
Consider these simple Entities:
@Entity
@Table(name = "system")
public class System {
@Id
@Column(name = "systemid", unique = true, nullable = false, length = 36)
public String systemid;
@ManyToOne(fetch = FetchType.LAZY, optional=false)
@JoinColumn(name = "systemtypeid", nullable = false)
public Systemtype systemtype;
//This column is added just for testing purposes, see comments below
@Column(name = "systemtypeid", insertable=false, updatable=false)
public String systemtypeid;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "system")
public Set<SystemItem> items = new HashSet<SystemItem>(0);
}
@Entity
@Table(name = "systemtype")
public class Systemtype {
@Id
@Column(name = "systemtypeid", unique = true, nullable = false, length = 36)
@Access(AccessType.PROPERTY)
public String systemtypeid;
public String getSystemtypeid() {
return systemtypeid;
}
public void setSystemtypeid(String systemtypeid) {
this.systemtypeid = systemtypeid;
}
@Column(name = "name", length = 60)
public String name;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "systemtype")
public Set<System> systems = new HashSet<System>(0);
}
@Entity
@Table(name = "systemitem")
public class SystemItem {
@Id
@Column(name = "systemitemid", unique = true, nullable = false, length = 36)
public String systemitemid;
@Column(name = "name", length = 60)
public String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "systemid", nullable = false)
public System system;
}
The controller:
@RestController
@RequestMapping(value = ApiController.SYSTEM_URL)
public class SystemController extends ApiController {
private static final Logger log = LoggerFactory.getLogger(SystemController.class);
@Autowired
private SystemService systemService;
@RequestMapping(method = RequestMethod.GET)
public Collection<System> getSystems() throws Exception {
List<System> systems = systemService.getSystems();
return systems;
}
}
And finally the service:
@Service
@Transactional(readOnly = true, value = "tmPrimary")
public class SystemService {
private static final Logger log = LoggerFactory.getLogger(SystemService.class);
@Autowired
SystemRepository systemRepository; //Spring-Data's PagingAndSortingRepository
public List<System> getSystems() {
return Lists.newArrayList(systemRepository.findAll());
}
}
So when the getSystems() method is called in the controller I was expecting to get a list with all Systems with only the basic fields filled since everything else is lazy loaded. And that's what happens, also checked the Hibernate query and the only table being queried is indeed the System table. So far so good.
But my first problem is that I was also expecting that system.systemtype.systemtypeid would be filled since this is a foreign key in the system table, but this is always null. If I was using EclipseLink I believe this would be the expected behavior, but it shouldn't be like that with Hibernate.
I added a dummy column to the System object (systemtypeid) for verification purposes and this indeed gets filled. So:
system.systemtypeid = "Something"
system.systemtype.systemtypeid = null
I think both of these should be "Something", so am I missing something here?
I figured this out meanwhile, Hibernate will only fetch the foreign key IDs if you annotate it to use property level access. This is stated here: https://developer.jboss.org/wiki/HibernateFAQ-TipsAndTricks#jive_content_id_How_can_I_retrieve_the_identifier_of_an_associated_object_without_fetching_the_association
I don't get this behavior when using Play, I'm now guessing Play might do some mumbo jumbo behind scenes to generate those.
The second problem starts once the data is being serialized to JSON in order to be sent to the client.
For starters I would expecting to be getting lazy intialization exceptions since the conversion is being done inside the controller and the transaction annotated method is on the service. Surprisingly that doesn't happen and the items set is lazily loaded and all items are serialized just fine to JSON.
Again can't understand this behavior, why is lazy load working at all when this is being done inside the controller? Does Spring Boot open a session at the start of the controller methods?
Also got this figured out (thanks to Nico for pointing me into the right direction), Spring Boot is enabling OpenSessionInView by default (really bad idea I must say) so I had to add spring.jpa.open-in-view=false to my applications.properties.
More surprisingly is that system.systemtype will still be empty after this ie won't lazy load at all, the only way I can get system.systemtype to load is if I declare it as eager.
I might be missing something obvious here, but I'm having a hard time understanding this behavior which is completely different from what I was experiencing when using Play and I would assume the behavior should be exactly the same.
UPDATE AFTER ALL EDITS:
The only remaining question is why system.systemtype will never lazy load. I did some tests and it will only lazy load IF I add getters/setters for all fields of the Systemtype entity. Is this normal?
Looking at the Hibernate docs it now seems it might be: https://docs.jboss.org/hibernate/orm/3.3/reference/en/html/performance.html
" By default, Hibernate3 uses lazy select fetching for collections and lazy proxy fetching for single-valued associations. These defaults make sense for most associations in the majority of applications. "
" Proxy fetching: a single-valued association is fetched when a method other than the identifier getter is invoked upon the associated object. "
So does this mean ALL entities used in single valued associations will need to have getters and setters defined for all their fields if I ever want to lazy load them? This sounds very confusing as it works in a different way on all other scenarios.
On Play I never needed to use getters/setters at all, so maybe Play was chaning the default fetching strategies for single valued associations? Is there even a way to do this?
fetch = FetchType.LAZY, that 's why it is null. can you post your hibernate configuration ?