Search This Blog

Tuesday 6 December 2011

One To Many association that is optional

We have seen one to many relations before but Hibernate also supports an optional one to many relationship. Consider chocolates and Bags. There is a possibility that some chocolates exists outside of a bag. Every chocolate can at any given time be in one and only one bag. However a particular chocolate may also not be in any bag. This makes the one to many relation an optional association.
One way to implement the above would be to make the foreign key column in chocolate table an optional field. However if you prefer to avoid null-able fields in your schema then an alternative approach is use a separate table to store this relation. Thus an entry is made to the table if and only if the chocolate is in a bag. The two classes for the same would be as before:
public class Bag {
    private Integer id;
    private String code;
    private Set<Chocolate> chocolates = new HashSet<Chocolate>();

    public synchronized void addChocolates(final Chocolate chocolate) {
        if (null != chocolate.getBag()) {
            this.removeChocolateFromBag(chocolate);
        }
        chocolate.setBag(this);
        chocolates.add(chocolate);
    }

    private synchronized void removeChocolateFromBag(final Chocolate chocolate) {
        chocolates.remove(chocolate);
    }
    //setter and getter methods   
}
The Chocolates class is
public class Chocolate {
    private Integer id;
    private String name;
    private String brand;
    private Bag bag;
    //setter and getter methods   
}
The mapping table is completely invisible to the java entities. They are managed from the hibernate mappings.
Chocolate.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 package="com.association.one_to_many.optional">
    <class name="Chocolate" table="CHOCOLATE">
        <id name="id" type="integer">
            <column name="ID" />
            <generator class="identity" />
        </id>
        <property name="name" type="string">
            <column name="NAME" />
        </property>

        <property name="brand" type="string">
            <column name="BRAND" />
        </property>

        <join table="BAG_CHOCOLATE" optional="true" inverse="true">
            <key column="CHOCOLATE_ID" unique="true" not-null="true" />
            <many-to-one name="bag" column="BAG_ID" foreign-key="BAG_CHOCOLATE_FK_1" />
        </join>
    </class>
</hibernate-mapping>
Each Chocolate has an optional many-to-one relation with Bag. Earlier this would have been achieved by a simple foreign key column. Here the join element is used, where the table attribute indicates the third table. Similar relation was used in the optional one to one relation that we saw in an earlier post. Here the unique attribute is not set. Optional attribute and not-null attribute is set to "true" to indicate that the relation is not always there. The inverse attribute is set here to ensure that the relation is not manged from here.
Bag.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 package="com.association.one_to_many.optional">
    <class name="Bag" table="BAG">
        <id name="id" type="integer">
            <column name="ID" />
            <generator class="identity" />
        </id>
        <property name="code" type="string">
            <column name="CODE" />
        </property>

        <set name="chocolates" table="BAG_CHOCOLATE" cascade="all">
            <key column="BAG_ID" /> <!-- Foreign Key -->
            <many-to-many class="Chocolate" column="CHOCOLATE_ID"
                unique="true" foreign-key="BAG_CHOCOLATE_FK_2" />
        </set>
    </class>
</hibernate-mapping>
A set of chocolates is present in the Bag. Instead of <one-to-many> we used a <many-to-many> as <one-to-many> does not include the column element. The unique="true" property ensures that Chocolate_Id value is unique(ensuring the one-to-many semantics). Cascade settings have also been included.
On start up the hibernate logs indicate the generated SQL:
    create table BAG (
        ID integer not null auto_increment,
        CODE varchar(255),
        primary key (ID)
    )
    create table BAG_CHOCOLATE (
        CHOCOLATE_ID integer not null unique,
        BAG_ID integer,
        primary key (BAG_ID, CHOCOLATE_ID)
    )
    create table CHOCOLATE (
        ID integer not null auto_increment,
        NAME varchar(255),
        BRAND varchar(255),
        primary key (ID)
    )
    alter table BAG_CHOCOLATE 
        add index BAG_CHOCOLATE_FK_1 (BAG_ID), 
        add constraint BAG_CHOCOLATE_FK_1 
        foreign key (BAG_ID) 
        references BAG (ID)
    alter table BAG_CHOCOLATE 
        add index BAG_CHOCOLATE_FK_2 (CHOCOLATE_ID), 
        add constraint BAG_CHOCOLATE_FK_2 
        foreign key (CHOCOLATE_ID) 
        references CHOCOLATE (ID)
If we create a Chocolate and a Bag without the association between them
static void createUnRelated() {
    Chocolate chocolate1 = new Chocolate();
    chocolate1.setName("Eclairs");
    chocolate1.setBrand("Cadburys");

    Bag chocolateBag = new Bag();
    chocolateBag.setCode("Bag01");

    Session session = sessionFactory.openSession();
    Transaction t = session.beginTransaction();
    session.save(chocolate1);
    session.save(chocolateBag);
    t.commit();
    System.out.println("The Chocolate Bag with name "
            + chocolateBag.getCode() + " was created with id "
            + chocolateBag.getId());
    System.out.println("Chocolate1 saved with id " + chocolate1.getId());
}
The logs indicate that only two insert SQL statements were fired:
2969 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        CHOCOLATE
        (NAME, BRAND) 
    values
        (?, ?)
...
3094 [main] DEBUG org.hibernate.id.IdentifierGeneratorFactory  - Natively genera
ted identity: 1
...
3219 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        BAG
        (CODE) 
    values
        (?)
...
3219 [main] DEBUG org.hibernate.id.IdentifierGeneratorFactory  - Natively genera
ted identity: 1
...
3281 [main] DEBUG org.hibernate.impl.SessionImpl  - after transaction completion
The Chocolate Bag with name Bag01 was created with id 1
Chocolate1 saved with id 1
To create the relationship between the two entities I execute the below code:
static void createRelationOnly() {    
    Session session = sessionFactory.openSession();
    Chocolate chocolate1 = (Chocolate) session.get(Chocolate.class, 1);
    Bag chocolateBag = (Bag) session.get(Bag.class, 1);
        // This will not trigger any saves because of the inverse
    chocolate1.setBag(chocolateBag);
        
    Transaction t = session.beginTransaction();
    chocolateBag.addChocolates(chocolate1);
    t.commit();
}
The logs indicate that the insert for the join table was fired:
2813 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        BAG_CHOCOLATE
        (BAG_ID, CHOCOLATE_ID) 
    values
        (?, ?)
...
2829 [main] DEBUG org.hibernate.type.IntegerType  - binding '1' to parameter: 1
2829 [main] DEBUG org.hibernate.type.IntegerType  - binding '1' to parameter: 2
The code to load the entities is as below:
static void testLoad() {
    Session session = sessionFactory.openSession();
    Chocolate chocolate1 = (Chocolate) session.get(Chocolate.class, 1);
    System.out.println("Chocolate " + chocolate1.getName()
            + " is from brand " + chocolate1.getBrand());
    System.out.println("The Bag is " + chocolate1.getBag().getCode());
    session.close();

    session = sessionFactory.openSession();
    Bag bag = (Bag) session.get(Bag.class, 1);
    System.out.println("The Bag is " + bag.getCode()
            + " and the number of chocolates are "
            + bag.getChocolates().size());
    session.close();
}
The queries fired are as below:
    select
        chocolate0_.ID as ID2_0_,
        chocolate0_.NAME as NAME2_0_,
        chocolate0_.BRAND as BRAND2_0_,
        chocolate0_1_.BAG_ID as BAG2_1_0_ 
    from
        CHOCOLATE chocolate0_ 
    left outer join
        BAG_CHOCOLATE chocolate0_1_ 
            on chocolate0_.ID=chocolate0_1_.CHOCOLATE_ID 
    where
        chocolate0_.ID=?

    select
        bag0_.ID as ID0_0_,
        bag0_.CODE as CODE0_0_ 
    from
        BAG bag0_ 
    where
        bag0_.ID=?
In the first session, the code to load the chocolate resulted in a join query that will return at most a single chocolate record. The join column will have value null/not null depending on the relation. Attempt to access the bag via the association results in an additional select query.
For the second session the queries are as below:
    select
        bag0_.ID as ID0_0_,
        bag0_.CODE as CODE0_0_ 
    from
        BAG bag0_ 
    where
        bag0_.ID=?

    select
        chocolates0_.BAG_ID as BAG2_1_,
        chocolates0_.CHOCOLATE_ID as CHOCOLATE1_1_,
        chocolate1_.ID as ID2_0_,
        chocolate1_.NAME as NAME2_0_,
        chocolate1_.BRAND as BRAND2_0_,
        chocolate1_1_.BAG_ID as BAG2_1_0_ 
    from
        BAG_CHOCOLATE chocolates0_ 
    left outer join
        CHOCOLATE chocolate1_ 
            on chocolates0_.CHOCOLATE_ID=chocolate1_.ID 
    left outer join
        BAG_CHOCOLATE chocolate1_1_ 
            on chocolate1_.ID=chocolate1_1_.CHOCOLATE_ID 
    where
        chocolates0_.BAG_ID=?
The attempt to load the Bag object will result will result in a simple select like above. Loading the one to many association will result in the second (double join) query:
    select
        bag0_.ID as ID0_0_,
        bag0_.CODE as CODE0_0_ 
    from
        BAG bag0_ 
    where
        bag0_.ID=?

    select
        chocolates0_.BAG_ID as BAG2_1_,
        chocolates0_.CHOCOLATE_ID as CHOCOLATE1_1_,
        chocolate1_.ID as ID2_0_,
        chocolate1_.NAME as NAME2_0_,
        chocolate1_.BRAND as BRAND2_0_,
        chocolate1_1_.BAG_ID as BAG2_1_0_ 
    from
        BAG_CHOCOLATE chocolates0_ 
    left outer join
        CHOCOLATE chocolate1_ 
            on chocolates0_.CHOCOLATE_ID=chocolate1_.ID 
    left outer join
        BAG_CHOCOLATE chocolate1_1_ 
            on chocolate1_.ID=chocolate1_1_.CHOCOLATE_ID 
    where
        chocolates0_.BAG_ID=?

To delete a bag, the code below was used:
static void deleteElements() {
    Session session = sessionFactory.openSession();
    Transaction t = session.beginTransaction();
    Bag chocolateBag = (Bag) session.get(Bag.class, 1);
        //cascade will take care of the rest
    session.delete(chocolateBag);
    t.commit();
}
The logs below indicate the select statement fired as a part of the load and the subsequent delete statements that followed:
2500 [main] DEBUG org.hibernate.SQL  - 
    delete 
    from
        BAG_CHOCOLATE 
    where
        BAG_ID=?
...
2531 [main] DEBUG org.hibernate.SQL  - 
    delete 
    from
        CHOCOLATE 
    where
        ID=?
2875 [main] DEBUG org.hibernate.SQL  - 
    delete 
    from
        BAG 
    where
        ID=?
First the records were removed from the link table, then the chocolates associated and then the bag record.

No comments:

Post a Comment