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!!

No comments:

Post a Comment