9

I have the following hibernate mapping.

@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "product")
private Set<ProductLicense> productLicenses = new HashSet<ProductLicense>(0);


@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "product_id", nullable = false)
private Product product;

But when I call product.getProductLicences() I always get an empty Set, even inside a transactional method.

sessionFactory.getCurrentSession().get(Product.class, productId))
            .getProductLicenses()

The following is the ProductDaoImpl class

@Repository
public class ProductDaoImpl implments ProductDao{

@Autowired
private SessionFactory sessionFactory;

@Override
public Product getProduct(Integer productId)
{

    Product result = (Product) sessionFactory.getCurrentSession().get(Product.class, productId);
    Hibernate.initialize(result.getProductLicenses());
            //sessionFactory.getCurrentSession().refresh(result);
    return result;

}
   .. other methods

}

And in addition if I call Hibernate.initialize(product); does not perform any join between the tables. The instruction sessionFactory.getCurrentSession().get(Product.class, productId); does not produce any query to the database (I have the property show_sql equals to true). But if I uncomment sessionFactory.getCurrentSession().refresh(result); I can see the sql and the set is loaded, and I cannot understand why in the other case is not loaded. But I cannot understand what is wrong in my mapping.

The product class is:

@Entity
@Table(name = "product")
public class Product {

@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "product_id", unique = true)
private Integer productId;

@NotNull
@Size(max = 200)
@Column(name = "name")
private String name;

    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "product")
private Set<ProductLicense> productLicenses = new HashSet<ProductLicense>(0);


    public Set<ProductLicense> getProductLicenses()
{
    return productLicenses;
}

public void setProductLicenses(Set<ProductLicense> productLicenses)
{
    this.productLicenses = productLicenses;
}

//getters and setters
..

}

ProductLicense class:

@Entity
@Table(name = "product_license")
public class ProductLicense {

@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "product_license_id", unique = true)
private Integer productLicenseId;

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "product_id", nullable = false)
    private Product product;

@NotNull
@Column(name = "key")
private String key;

    // getters and setters ...

}

And finally my configuration is the following:

 @Configuration
 @EnableWebMvc
 @EnableTransactionManagement
 @ComponentScan("com.company.package")
 @PropertySource("classpath:application.properties")

public class WebAppConfig {

private static final String PROPERTY_NAME_DATABASE_DRIVER = "db.driver";
private static final String PROPERTY_NAME_DATABASE_PASSWORD = "db.password";
private static final String PROPERTY_NAME_DATABASE_URL = "db.url";
private static final String PROPERTY_NAME_DATABASE_USERNAME = "db.username";

private static final String PROPERTY_NAME_HIBERNATE_DIALECT = "hibernate.dialect";
private static final String PROPERTY_NAME_HIBERNATE_SHOW_SQL = "hibernate.show_sql";
private static final String PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN = "entitymanager.packages.to.scan";

@Resource
private Environment env;

@Bean
public DataSource dataSource()
{
    DriverManagerDataSource dataSource = new DriverManagerDataSource();

    dataSource.setDriverClassName(env.getRequiredProperty(PROPERTY_NAME_DATABASE_DRIVER));
    dataSource.setUrl(env.getRequiredProperty(PROPERTY_NAME_DATABASE_URL));
    dataSource.setUsername(env.getRequiredProperty(PROPERTY_NAME_DATABASE_USERNAME));
    dataSource.setPassword(env.getRequiredProperty(PROPERTY_NAME_DATABASE_PASSWORD));

    return dataSource;
}

private Properties hibProperties()
{
    Properties properties = new Properties();
    properties.put(PROPERTY_NAME_HIBERNATE_DIALECT,
            env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_DIALECT));
    properties.put(PROPERTY_NAME_HIBERNATE_SHOW_SQL,
            env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_SHOW_SQL));
    properties.put("hibernate.cache.provider_class", "org.hibernate.cache.NoCacheProvider");
    properties.put("hibernate.cache.use_second_level_cache", "false");
    return properties;
}

@Bean
public LocalSessionFactoryBean sessionFactory()
{
    LocalSessionFactoryBean lsfb = new LocalSessionFactoryBean();
    lsfb.setDataSource(this.dataSource());
    lsfb.setPackagesToScan(env.getRequiredProperty(PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN));
    lsfb.setHibernateProperties(this.hibProperties());
    return lsfb;
}

@Bean
public HibernateTransactionManager transactionManager()
{
    return new HibernateTransactionManager(this.sessionFactory().getObject());
}
 }

My Test:

@Before
@Transactional
public void setUp() throws ParseException
{

    product1 = new Product();
    ..

    productService.addProduct(product1);

    product2 = new Product();
    ..

    product2 = productService.addProduct(product2);

    productService.addProductLicense(product2.getProductId(), licenceKey1Product2);
    productService.addProductLicense(product2.getProductId(), licenceKey2Product2);



}

  @Test
@Transactional
public void addProductLicenseTest()
{
    String licenseKey[] = { "thisisthekey", "thisisthekey2" };

    productService.addProductLicense(product1.getProductId(), licenseKey[0]);

            //sessionFactory.getCurrentSession().flush();
    //sessionFactory.getCurrentSession().clear();

    Product product1saved = productService.getProduct(product1.getProductId());

    assertEquals(1, product1saved.getProductLicenses().size());
    assertEquals(licenseKey[0], product1saved.getProductLicenses().iterator().next().getKey());

    productService.addProductLicense(product1.getProductId(), licenseKey[1]);

    product1saved = productService.getProduct(product1saved.getProductId());

    assertEquals(2, product1saved.getProductLicenses().size());

 }
cloudy_weather
  • 2,837
  • 12
  • 38
  • 63
  • How do you test that? – JB Nizet Feb 25 '14 at 10:51
  • on Junit in a test marked with '@Test' and '@Transactional' I call Product product1saved = productService.getProduct(product1.getProductId()); – cloudy_weather Feb 25 '14 at 10:55
  • Show us the complete code of this test, by editing your question – JB Nizet Feb 25 '14 at 11:06
  • @JBNizet I have noticed that adding sessionFactory.getCurrentSession().flush(); sessionFactory.getCurrentSession().clear(); before productService.getProduct(product1.getProductId()); in the unit test helped me a lot, but now I am wondering why hibernate has this strange behaviour with junit? – cloudy_weather Feb 25 '14 at 12:17
  • I'm probably able to explain why your code behaves like you're seeing, but not without seeing the code. Post the code of the test. – JB Nizet Feb 25 '14 at 12:35

1 Answers1

9

Here's what (most probably) happens.

Your whole test method is transactional. The calls of the service thus happen in the same transaction as the code of the test itself.

In the test, you're creating and persisting a Product, without any license. This stores a Product instance in the session cache, tied to the transaction. This product has an empty list of licenses.

Then you call a service method, still inside the same transaction, that creates a license whose product is the produc you created previously. My guess is that the code of this service looks like the following:

Product product = (Product) session.get(Product.class, productId);
ProductLicense license = new ProductLicense();
license.setProduct(product);
session.persist(license);

In the code above, the product is obtained from the session cache, and the license is then persisted with the found product. Note that the code sets the product of the license, but it doesn't add the license to the product, which is the problem.

Then you're getting the product by ID, still in the same transaction. Hibernate thus retrieves the product directly from the cache. And in the cache, since you omitted to add the license to the product, the license list is still empty.

So, in short, it's your responsibility to maintain both sides of the bidirectional association. If you only initialize the owner side, Hibernate will persist the association, and will initialize the list correctly if it loads the product from the database. But while the product is in the cache, it stays as you stored it in the cache. This is why you see an empty list, unless you explicitely clear the session and thus reload the state from the database.

JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
  • Yes, I agree with you. But it is something that happens in the first level of the Hibernate cache. Therefore I guess that I have this strange behaviour only for the unit tests because they are executed under the same transaction. – cloudy_weather Feb 25 '14 at 16:47
  • 1
    If your production code also adds a license, and also expects the license to be in the product list, then you'll have the same problem in the production code. It's up to you. – JB Nizet Feb 25 '14 at 17:43
  • sorry for the silly observation, but if in my code after the session.update(product); I add session.flush(); will it help me or not? my suspect is that there is no way that hibernate can update the one to many relationship in both sides for me. Therefore it means that after the code that you have written above I have to add a product.getLicences().add(license); session.update(product); it is a bit odd, don't I need to perform only session.update(license); ? – cloudy_weather Feb 26 '14 at 09:10
  • No, a flush won't help. flush only executes the pensing insert/delete/update queries. Hibernat won't update both sides for you. It's your responsibility to do that. – JB Nizet Feb 26 '14 at 11:30
  • I have same issue, flush does not help, how to easly reload it on JPA? I use unidirectional with EAGER – Kamil Nękanowicz Dec 29 '16 at 08:09
  • @KamilNekanowicz you can use session.evict(entity) or session.refresh(entity). Entity will be the entity that you don't want to get from cache. – H.Ç.T Dec 16 '19 at 12:54