-->
These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 3 posts ] 
Author Message
 Post subject: Truly Lazy-loading Maps (in a multilingual site)
PostPosted: Sat Oct 18, 2008 3:08 pm 
Newbie

Joined: Sat Oct 18, 2008 11:34 am
Posts: 10
Dear all!

I've been trying for a while to find a solution to this problem, but with no success. It is quite strange how googling around did not give me any useful information, giving the fact that we are not talking about anything exotic here, but quite a common problem: making a multilingual application (or a site, in my case). Basically, it comes down to the question of making a truly lazy-loading Map, that loads just the addressed entries instead of the whole Map.

I would really appreciate some help here; moreover, believing that this post would be helpful to more people out there...

Let's take a catalog of Persons that can be browsed by users. My idea is to put a Map<Locale, String> in the Person object, containing translations for different locales, so that a biography for a required locale could be fetched by a getBiography(Locale loc) method. But alas, the problem with that approach is that accessing the Map to fetch just one translation, loads the entire map!

So the big question is how to achieve something like a truly lazy-loading map, that is, a proxy that would load just load entries addressed by the Map.get(key) method?

I am using Hibernate Core 3.3.1 GA with Annotations 3.4.0 GA.

Code:
package test;

import java.io.*;
import java.util.*;
import javax.persistence.*;
import org.hibernate.annotations.*;
import org.hibernate.validator.*;

@Entity
@Proxy(lazy = true)
public class Person implements Serializable {

   private Long id;
   private String name;
   private Map<Locale, String> biographyMap = new HashMap<Locale, String>();

   @Id
   @GeneratedValue
   public Long getId() {
      return id;
   }
   public void setId(Long id) {
      this.id = id;
   }

   @Length(max = 20)
   public String getName() {
      return name;
   }
   public void setName(String name) {
      this.name = name;
   }

   @CollectionOfElements
   @JoinTable(name = "PERSON_BIOGRAPHY", joinColumns = @JoinColumn(name = "PERSON_ID"))
   @MapKey(columns = @Column(name = "LOCALE", length = 8))
   @Column(name = "BIOGRAPHY")
   public Map<Locale, String> getBiographyMap() {
      return biographyMap;
   }
   public void setBiographyMap(Map<Locale, String> biographyMap) {
      this.biographyMap = biographyMap;
   }

   public void setBiography(Locale loc, String txt) {
      getBiographyMap().put(loc, txt);
   }

   // TODO: How to make this load just a required entry?
   public String getBiography(Locale loc) {
      return getBiographyMap().get(loc);
   }

   // TODO: How to make this return just the keys, without loading the entries?
   public Set<Locale> getAvailableBiographyLocales() {
      return getBiographyMap().keySet();
   }
}


Any help is appreciated!


Top
 Profile  
 
 Post subject:
PostPosted: Sat Oct 18, 2008 4:28 pm 
Expert
Expert

Joined: Wed Mar 03, 2004 6:35 am
Posts: 1240
Location: Lund, Sweden
You can try to make the collection lazy="extra" (hbm style, don't know it's counterpart with annotations). According to the docs this is a feature "where most operations do not initialize the collection". I have no idea if it will help in your case but it is wort a try.


Top
 Profile  
 
 Post subject:
PostPosted: Sat Oct 18, 2008 5:09 pm 
Newbie

Joined: Sat Oct 18, 2008 11:34 am
Posts: 10
nordborg wrote:
You can try to make the collection lazy="extra" (hbm style, don't know it's counterpart with annotations). According to the docs this is a feature "where most operations do not initialize the collection". I have no idea if it will help in your case but it is wort a try.

Thanks, mate! It actually works! :-)

If I wasn't too lazy to read the Jave Persistence with Hibernate completely, I would have found it saying:

Java Persistence with Hibernate, on page 567 wrote:
...The collection wrapper is now smarter than before. The collection is no longer initialized if you call size(), contains(), or isEmpty()—the database is queried to retrieve the necessary information. If it’s a Map or a List, the operations containsKey() and get() also query the database directly...

The only problem with that book is it being sooo comprehensive... It's not the easiest literature on the subject, but it's great once you read it.

I just had to change the code above by adding a @LazyCollection(LazyCollectionOption.EXTRA) at the biographyMap getter:
Code:
   @CollectionOfElements
   @LazyCollection(LazyCollectionOption.EXTRA)
   @JoinTable(name = "PERSON_BIOGRAPHY", joinColumns = @JoinColumn(name = "PERSON_ID"))
   @MapKey(columns = @Column(name = "LOCALE", length = 8))
   @Column(name = "BIOGRAPHY")
   public Map<Locale, String> getBiographyMap() {
      return biographyMap;
   }

Executing the following piece of code:
Code:
      ...
      List<Person> persons = em.createQuery("select p from Person p order by p.name asc").getResultList();
      System.out.println(persons.size() + " person(s) found:");

      Person loadedPerson = persons.get(0);
      System.out.println("Fetching biography for " + loadedPerson.getName());
      System.out.println("Loaded biography: " + loadedPerson.getBiographyMap().get(new Locale("en")));
      System.out.println("There are "+loadedPerson.getBiographyMap().size()+" translations");

...gets the following result:
Code:
Hibernate: select person0_.id as id2_, person0_.name as name2_, person0_.version as version2_ from Person person0_ order by person0_.name asc
41 person(s) found:
Fetching biography for John Doe
Hibernate: select BIOGRAPHY from PERSON_BIOGRAPHY where PERSON_ID =? and LOCALE =?
Loaded biography: English biography...
Hibernate: select count(BIOGRAPHY) from PERSON_BIOGRAPHY where PERSON_ID =?
There are 3 translations

Obviously, the Hibernate first loads the Person object, then loads just a single row when accessing a map by a specific key. Also, calling a map.size() just queries the database to count the rows.

Unfortunately, map.keySet() loads the entire map together with the mapped objects, so querying a map keys must be done by writing a direct SQL:
Code:
      Query q = getSession(em).createSQLQuery(
            "select LOCALE from PERSON_BIOGRAPHY where PERSON_ID=:id").addScalar("LOCALE", Hibernate.LOCALE);
      q.setParameter("id", personId);
      List<Locale> list = q.list();
      System.out.println("Available locales: " + list);


Thanks for the help!


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 3 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.