Friday, June 26, 2009

Compounding Hibernate Problems 2: The Enterprise Solution Strikes Back

It is a dark time for the
developers. Although Hibernate
can handle simple schema designs,
existing databases have driven the
coders from their lighthearted
news aggregation sites to
pursue answers from across
the internet.

Again I’m trying to add some table relations with Hibernate’s Enterprise handling of composite primary keys getting in the way.

Back with good ol' Derby.

jdbc:derby://localhost:1527/MyDB;create=true

username=myschema
password=whatever
ddl
create table vehicle (
    make_id integer,
    model_id integer,
    name varchar(50),
    PRIMARY KEY (make_id, model_id)
);

create table maker_country (
    make_id integer,
    country_id integer,
    make_country varchar(50),
    PRIMARY KEY (make_id, country_id)
);


insert into vehicle values (1, 0, 'Chevrolet Corvette');
insert into vehicle values (1, 1, 'Chevrolet Malibu');
insert into vehicle values (2, 0, 'Ford Focus');
insert into vehicle values (2, 1, 'Ford Mustang');
insert into vehicle values (3, 0, 'Honda Accord');
insert into vehicle values (3, 1, 'Honda Odyssey');
insert into vehicle values (4, 0, 'Toyota Avalon');
insert into vehicle values (4, 1, 'Toyota Camry');
insert into vehicle values (5, 1, 'Volkswagen Bug');

insert into maker_country values (1, 1, 'Chevrolet makes cars for USA');
insert into maker_country values (2, 1, 'Ford makes cars for USA');
insert into maker_country values (3, 2, 'Honda makes cars for Japan');
insert into maker_country values (3, 1, 'Honda makes cars for USA');
insert into maker_country values (4, 2, 'Toyota makes cars for Japan');
insert into maker_country values (4, 1, 'Toyota makes cars for USA');
insert into maker_country values (5, 3, 'Volkswagen makes cars for Germany');
insert into maker_country values (5, 1, 'Volkswagen makes cars for USA');
.cfg.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
        <property name="hibernate.connection.driver_class">org.apache.derby.jdbc.EmbeddedDriver</property>
        <property name="hibernate.dialect">org.hibernate.dialect.DerbyDialect</property>
        <property name="hibernate.connection.url">jdbc:derby://localhost:1527/MyDB;create=true</property>
        <property name="hibernate.connection.username">myschema</property>
        <property name="hibernate.connection.password">whatever</property>
        <property name="hibernate.default_schema">MYSCHEMA</property>
        <property name="hibernate.current_session_context_class">thread</property>
        <property name="hibernate.show_sql">true</property>
    </session-factory>
</hibernate-configuration>

Of course we know this won’t work…

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-reverse-engineering PUBLIC
    "-//Hibernate/Hibernate Reverse Engineering DTD 3.0//EN"
    "http://hibernate.sourceforge.net/hibernate-reverse-engineering-3.0.dtd" >

<hibernate-reverse-engineering>
    <table-filter match-name="MAKER_COUNTRY"/>
    <table-filter match-name="VEHICLE"/>

    <table name="VEHICLE">
        <foreign-key foreign-table="MAKER_COUNTRY">
            <column-ref local-column="MAKE_ID" foreign-column="MAKE_ID" />
        </foreign-key>
    </table>

</hibernate-reverse-engineering>
Foreign key (FK3F2F1B0C64FEF3DE:VEHICLE [MAKE_ID])) must have same number of columns as the referenced primary key (MAKER_COUNTRY [MAKE_ID,COUNTRY_ID])

So we stick with the default generated reverse engineering. We can use it to generate only the mapping files (no .java classes yet).

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-reverse-engineering PUBLIC
    "-//Hibernate/Hibernate Reverse Engineering DTD 3.0//EN"
    "http://hibernate.sourceforge.net/hibernate-reverse-engineering-3.0.dtd" >

<hibernate-reverse-engineering>
    <table-filter match-name="MAKER_COUNTRY"/>
    <table-filter match-name="VEHICLE"/>
</hibernate-reverse-engineering>

Then we can edit the mapping file to add a relationship between two tables.

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
    <class name="Vehicle" table="VEHICLE">
        <composite-id name="id" class="VehicleId">
            <key-property name="makeId" type="int">
                <column name="MAKE_ID" />
            </key-property>
            <key-property name="modelId" type="int">
                <column name="MODEL_ID" />
            </key-property>
        </composite-id>
        <property name="name" type="string">
            <column name="NAME" length="50" />
        </property>

        <!-- Added this: -->
        <set name="makerCountries" inverse="false" lazy="true">
            <key property-ref="makeId" column="MAKE_ID" />
            <one-to-many class="MakerCountry" />
        </set>

    </class>
</hibernate-mapping>

No problem, right?

org.hibernate.MappingException: property-ref [makeId] not found on entity [Vehicle]

Ok, so it can’t see its own property. What if reference its key’s property.

        ...
            <key property-ref="id.makeId" column="MAKE_ID" />
        ...

It generated some classes. Let’s give it a run.

Test.java
import java.net.URL;
import java.util.List;
import java.util.Set;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.AnnotationConfiguration;

public class Test {
    static SessionFactory sessionFactory;
    static Session session;
    public static void main(String[] args) {
        URL url = Test.class.getResource("/hibernate.cfg.xml");
        sessionFactory = new AnnotationConfiguration().configure(url).buildSessionFactory();
        session = sessionFactory.getCurrentSession();
        org.hibernate.Transaction tx = session.beginTransaction();

        Query q = session.createQuery("from Vehicle as v where v.id.makeId = 1");
        List<Vehicle> lst = null;
        lst = q.list();
        if (lst.size() > 0) {
            Vehicle v = lst.get(0);
            System.out.println(v.getName());
            Set<MakerCountry> makerCountries = v.getMakerCountries();
            System.out.println(makerCountries.size());
        }
    }
}

Nope.

Hibernate: select vehicle0_.MAKE_ID as MAKE1_1_, vehicle0_.MODEL_ID as MODEL2_1_, vehicle0_.NAME as NAME1_ from MYSCHEMA.VEHICLE vehicle0_ where vehicle0_.MAKE_ID=1
Exception in thread "main" org.hibernate.HibernateException: Unable to resolve property: id.makeId

Fine, we’ll add the property ourselves.

        ...
        <property name="makeId" type="int" insert="false" update="false">
            <column name="MAKE_ID" />
        </property>
        <set name="makerCountries" inverse="false" lazy="true">
            <key property-ref="makeId" column="MAKE_ID" />
            <one-to-many class="MakerCountry" />
        </set>
        ...

Now umm…

Hibernate: select vehicle0_.MAKE_ID as MAKE1_1_, vehicle0_.MODEL_ID as MODEL2_1_, vehicle0_.NAME as NAME1_ from MYSCHEMA.VEHICLE vehicle0_ where vehicle0_.MAKE_ID=1
Chevrolet Corvette
Exception in thread "main" org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: Vehicle.makerCountries, no session or session was closed

"no session or session was closed"?? What does that mean?

Whatever, we’ll set lazy to false.

        ...
        <set name="makerCountries" inverse="false" lazy="false">
        ...

and…

Hibernate: select vehicle0_.MAKE_ID as MAKE1_1_, vehicle0_.MODEL_ID as MODEL2_1_, vehicle0_.NAME as NAME1_ from MYSCHEMA.VEHICLE vehicle0_ where vehicle0_.MAKE_ID=1
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer

I think Luke Skywalker summed it up quite poignantly when he said

NOOOOOOOOOOOOOO!!

Tuesday, June 23, 2009

Lazy Hibernate

Let’s play with large collections.

ddl
CREATE TABLE popular_band (
    band_id AS INTEGER PRIMARY KEY,
    band_name AS VARCHAR
);

CREATE TABLE fan (
    fan_id AS INTEGER PRIMARY KEY,
    fan_name AS VARCHAR,
    favorite_band_id AS INTEGER
);

ALTER TABLE fan ADD fans_fk_popular_band FOREIGN KEY (favorite_band_id)
REFERENCES popular_band(band_id);

INSERT INTO popular_band VALUES (1, "Beatles");

INSERT INTO fan VALUES (1, "Jim", 1);
INSERT INTO fan VALUES (2, "Bob", 1);
INSERT INTO fan VALUES (3, "Sue", 1);
INSERT INTO fan VALUES (4, "Mary", 1);
-- 100,000 more ...
INSERT INTO fan VALUES (100005, "Alex", 1);

After reverse engineering this schema with Hibernate, your mappings should look something like this.

PopularBand.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
    <class name="PopularBand" table="POPULAR_BAND">
        <id name="bandId" type="long">
            <column name="BAND_ID" />
        </id>
        <property name="bandName" type="string">
            <column name="BAND_NAME" />
        </property>
        <set name="fans" inverse="true" lazy="true" table="FAN" fetch="select">
            <key>
                <column name="FAVORITE_BAND_ID" precision="10" scale="0" />
            </key>
            <one-to-many class="Fan" />
        </set>
    </class>

    <class name="Fan" table="FAN">
        <id name="fanId" type="long">
            <column name="FAN_ID" />
        </id>
        <property name="fanName" type="string">
            <column name="FAN_NAME" />
        </property>
        <many-to-one name="favoriteBand" class="PopularBand" fetch="select">
            <column name="FAVORITE_BAND_ID" />
        </many-to-one>
    </class>
</hibernate-mapping>

The Beatles, being a very popular band, have many fans. I just want to look at 10, in no particular order. That shouldn’t be hard, right?

public static void print10fans(PopularBand pb) {
    Set<Fan> fans = pb.getFans();
    Iterator<Fan> iter = fans.iterator();
    for (int i=0; i < 10 && iter.hasNext(); i++) {
        System.out.println(iter.next().getFanName());
    }
}

And the output is

java.lang.OutOfMemoryError: Java heap space
...

Oops, our very large collection is very large. Maybe the manual can help us.

lazy (optional - defaults to true) may be used to disable lazy fetching and specify that the association is always eagerly fetched, or to enable "extra-lazy" fetching where most operations do not initialize the collection (suitable for very large collections)

Perfect! Just a quick change

<set name="fans" inverse="true" lazy="extra" table="FAN" fetch="select">
                                      ^^^^^

and we can try again.

java.lang.OutOfMemoryError: Java heap space
...

Ok, just what does this "extra-lazy" really mean anyway? The manual is very silent on such details, so we turn to other sources (emphasis added).

Extra lazy doesn’t do much on sets. It is actually quite useful only on the size method which triggers a select count on the database. However, iterating over a set will instantiate the whole collection. On maps and lists, extra lazy also enables fetching an entry by index. Still iterating over a list or a map will fetch the whole collection from database.

So when the manual says

…most operations do not initialize the collection (suitable for very large collections)

it actually means

…only the size() method does not initialize the collection, unless it’s a map or list, then you can also fetch by index (but otherwise your very large collection is destined to crash your program)

You can use "Filtering collections" to specify how many results you want, but doing that seems to turn the whole idea of “Plain-Old-Java-Objects” into “Plain-Old-Java-Objects-Unless-Your-Collections-Are-Too-Big-Then-You-Have-To-Use-Framework-Specific-API”