Advanced Hibernate, Maps Part 2: Query by Lazy Map

Maarten Winkels

Hibernate is a very mature and feature rich product that can be used to solve a lot of basic or advanced problems. One of its core features is the ability to map collections. If an entity contains a collection, for example a Map, Hibernate will map the entity and the map onto a relational structure (a number of tables) and allows the application to navigate through the collection without having to make explicit JDBC calls. Hibernate will under the hood perform the correct queries to fetch the data or even update rows in the database according to changes made by the application on the data structure in memory.

But what if you have a very large collection that you need to search for a single entry? For example, say the system is used to fetch the PostalCode a particular Street in a City. Fetching the complete collection of Streets and PostalCodes just to find out the PostalCode for a single street is not a very attractive idea. The normal approach would probably be to use a DAO and make an explicit query. Apart from the problem of getting the DAO object injected into the City entity, using a lazy Map is a more elegant solution.

Let's look at some code examples.

public class City {
  private Map postalCodes;
  public void add(Street street, PostalCode code) {
    postalCodes.put(street, code);
  }
  public PostalCode getPostalCode(Street street) {
    return postalCodes.get(street);
  }
}

public class Street {
  public String name;
}

public class PostalCode {
  public String code;
}

Some essential parts of code (like constructors and the like) have been left out for simplicity.
Now how dow we map these structures? Lets look at the hibernate mapping.


  
    
      
    
    
    
      
      
        
      
      
        
      
    
  

The Map is mapped as a collection of composites. Also notice the lazy="extra" setting used on the map. When we run the following test

City city = new City("Nijmegen");
city.add(new Street("KronenburgerSingel"), new PostalCode("6511 AR"));
city.add(new Street("Markt"), new PostalCode("6511 AA"));
city.add(new Street("Keizer Karel Plein"), new PostalCode("6511 BD"));

Session session = factory.openSession();
Transaction transaction = session.beginTransaction();
session.save(city);
transaction.commit();
session.close();

session = factory.openSession();
City nijmegen = (City) session.createCriteria(City.class).add(Restrictions.eq("name", "Nijmegen")).uniqueResult();

assertEquals("6511 AA", nijmegen.getPostalCode(new Street("Markt")).code);
assertEquals("6511 BD", nijmegen.getPostalCode(new Street("Keizer Karel Plein")).code);
assertEquals("6511 AA", nijmegen.getPostalCode(new Street("Markt")).code);

The console shows the following:
DEBUG SchemaExport:301 - create table City (id bigint generated by default as identity (start with 1), name varchar(255), primary key (id))
DEBUG SchemaExport:301 - create table postalCodes (CITY_FK bigint not null, code varchar(255), STREET varchar(255) not null, primary key (CITY_FK, STREET))
DEBUG SchemaExport:301 - alter table postalCodes add constraint FK84186A1B96E53CE4 foreign key (CITY_FK) references City
...
DEBUG SQL:346 - insert into City (name, id) values (?, null)
DEBUG StringType:80 - binding 'Nijmegen' to parameter: 1
DEBUG SQL:346 - call identity()
DEBUG SQL:346 - insert into postalCodes (CITY_FK, STREET, code) values (?, ?, ?)
DEBUG LongType:80 - binding '1' to parameter: 1
DEBUG StringType:80 - binding 'Markt' to parameter: 2
DEBUG StringType:80 - binding '6511 AA' to parameter: 3
DEBUG SQL:346 - insert into postalCodes (CITY_FK, STREET, code) values (?, ?, ?)
DEBUG LongType:80 - binding '1' to parameter: 1
DEBUG StringType:80 - binding 'Keizer Karel Plein' to parameter: 2
DEBUG StringType:80 - binding '6511 BD' to parameter: 3
DEBUG SQL:346 - insert into postalCodes (CITY_FK, STREET, code) values (?, ?, ?)
DEBUG LongType:80 - binding '1' to parameter: 1
DEBUG StringType:80 - binding 'KronenburgerSingel' to parameter: 2
DEBUG StringType:80 - binding '6511 AR' to parameter: 3
...
DEBUG SQL:346 - select this_.id as id0_0_, this_.name as name0_0_ from City this_ where this_.name=?
DEBUG StringType:80 - binding 'Nijmegen' to parameter: 1
DEBUG LongType:122 - returning '1' as column: id0_0_
DEBUG StringType:122 - returning 'Nijmegen' as column: name0_0_
DEBUG SQL:346 - select code from postalCodes where CITY_FK =? and STREET =?
DEBUG LongType:80 - binding '1' to parameter: 1
DEBUG StringType:80 - binding 'Markt' to parameter: 2
DEBUG StringType:122 - returning '6511 AA' as column: code
DEBUG SQL:346 - select code from postalCodes where CITY_FK =? and STREET =?
DEBUG LongType:80 - binding '1' to parameter: 1
DEBUG StringType:80 - binding 'Keizer Karel Plein' to parameter: 2
DEBUG StringType:122 - returning '6511 BD' as column: code
DEBUG SQL:346 - select code from postalCodes where CITY_FK =? and STREET =?
DEBUG LongType:80 - binding '1' to parameter: 1
DEBUG StringType:80 - binding 'Markt' to parameter: 2
DEBUG StringType:122 - returning '6511 AA' as column: code

First the two tables are created. The second table represents the Map with both components in it. The PK of this table is the FK to the containing entity and the columns representing the key in the map, in this case the STREET column that maps to the Street.name property.
The rows are inserted as expected.

Now when querying the data (we're using a new session and a new entity instance) each get on the Map results in a new query on the database. If we do the same request twice, the same query is executed twice. This is because hibernate does not keep the Map in memory. For big collections this behavior can be very useful.

There are two drawbacks with this approach:
1. A collection mapped using Hibernate must always have an owner entity. The Map cannot be a standalone entity and it cannot be shared between entities. This can potentially lead to multiplying the relational structures if the data is used in several entities.
2. An extra lazy Map cannot be updated in an extra lazy fashion. This means that adding en entry to the Map or removing an entry from the Map will trigger Hibernate to load the complete Map into memory. Also, if components are used, Hibernate will not be able to detect changes to these objects, so this code

session = factory.openSession();
transaction = session.beginTransaction();
stad = (City) session.createCriteria(City.class).add(Restrictions.eq("name", "Nijmegen")).uniqueResult();
stad.getPostalCode(new Street("KronenburgerSingel")).code = "6511 AP";
transaction.commit();

will not result in an update on the database. If the PostalCode class was promoted to an entity, Hibernate would be able to detect the changes and do the update.

Comments (0)

    Add a Comment