7.3. Polymorphic associations

Polymorphism is a defining feature of object-oriented languages like Java. Support for polymorphic associations and polymorphic queries is an absolutely basic feature of an ORM solution like Hibernate. Surprisingly, we've managed to get this far without needing to talk much about polymorphism. Even more surprisingly, there is not much to say on the topic—polymorphism is so easy to use in Hibernate that we don't need to spend a lot of effort explaining it.

To get an overview, we first consider a many-to-one association to a class that may have subclasses. In this case, Hibernate guarantees that you can create links to any subclass instance just like you would to instances of the superclass.

7.3.1. Polymorphic many-to-one associations

A polymorphic association is an association that may refer instances of a subclass of the class that was explicitly specified in the mapping metadata. For this example, consider the defaultBillingDetails property of User. It references one particular BillingDetails object, which at runtime can be any concrete instance of that class. The classes are shown in figure 7.13.

You map this association to the abstract class BillingDetails as follows in User.hbm.xml.

Figure 7-13. A user has either a credit card or a bank account as the default.

<many-to-one name="defaultBillingDetails"
             class="BillingDetails"
             column="DEFAULT_BILLING_DETAILS_ID"/>

But because BillingDetails is abstract, the association must refer to an instance of one of its subclasses—CreditCard or CheckingAccount—at runtime.

You don't have to do anything special to enable polymorphic associations in Hibernate; specify the name of any mapped persistent class in your association mapping (or let Hibernate discover it using reflection), and then, if that class declares any <union-subclass>, <subclass>, or <joined-subclass> elements, the association is naturally polymorphic.

The following code demonstrates the creation of an association to an instance of the CreditCard subclass:

CreditCard cc = new CreditCard();
cc.setNumber(ccNumber);
cc.setType(ccType);
cc.setExpiryDate(ccExpiryDate);

User user = (User) session.get(User.class, userId);
user.addBillingDetails(cc); // Add it to the one-to-many association

user.setDefaultBillingDetails(cc);

// Complete unit of work

Now, when you navigate the association in a second unit of work, Hibernate automatically retrieves the CreditCard instance:

User user = (User) secondSession.get(User.class, userId);

// Invoke the pay() method on the actual subclass instance
user.getDefaultBillingDetails().pay(amount);

There is just one thing to watch out for: If BillingDetails was mapped with lazy="true" (which is the default), Hibernate would proxy the defaultBillingDetails association target. In this case, you wouldn't be able to perform a typecast to the concrete class CreditCard at runtime, and even the instanceof operator would behave strangely:

User user = (User) session.get(User.class, userid);
BillingDetails bd = user.getDefaultBillingDetails();
System.out.println( bd instanceof CreditCard ); // Prints "false"
CreditCard cc = (CreditCard) bd; // ClassCastException!

In this code, the typecast fails because bd is a proxy instance. When a method is invoked on the proxy, the call is delegated to an instance of CreditCard that is fetched lazily (it's an instance of a runtime-generated subclass, so instanceof also fails). Until this initialization occurs, Hibernate doesn't know what the subtype of the given instance is—this would require a database hit, which you try to avoid with lazy loading in the first place. To perform a proxy-safe typecast, use load():

User user = (User) session.get(User.class, userId);
BillingDetails bd = user.getDefaultBillingDetails();

// Narrow the proxy to the subclass, doesn't hit the database
CreditCard cc =
    (CreditCard) session.load( CreditCard.class, bd.getId() );
expiryDate = cc.getExpiryDate();

After the call to load(), bd and cc refer to two different proxy instances, which both delegate to the same underlying CreditCard instance. However, the second proxy has a different interface, and you can call methods (like getExpiryDate()) that apply only to this interface.

Note that you can avoid these issues by avoiding lazy fetching, as in the following code, using an eager fetch query:

User user = (User)session.createCriteria(User.class)
    .add(Restrictions.eq("id", uid) )
    .setFetchMode("defaultBillingDetails", FetchMode.JOIN)
    .uniqueResult();

// The users defaultBillingDetails have been fetched eagerly
CreditCard cc = (CreditCard) user.getDefaultBillingDetails();
expiryDate = cc.getExpiryDate();

Truly object-oriented code shouldn't use instanceof or numerous typecasts. If you find yourself running into problems with proxies, you should question your design, asking whether there is a more polymorphic approach. Hibernate also offers bytecode instrumentation as an alternative to lazy loading through proxies; we'll get back to fetching strategies in chapter 13, section 13.1, "Defining the global fetch plan."

One-to-one associations are handled the same way. What about many-valued associations—for example, the collection of billingDetails for each User?

7.3.2. Polymorphic collections

A User may have references to many BillingDetails, not only a single default (one of the many is the default). You map this with a bidirectional one-to-many association.

In BillingDetails, you have the following:

<many-to-one name="user"
             class="User"
             column="USER_ID"/>

In the Users mapping you have:

<set name="billingDetails"
     inverse="true">
    <key column="USER_ID"/>
    <one-to-many class="BillingDetails"/>
</set>

Adding a CreditCard is easy:

CreditCard cc = new CreditCard();
cc.setNumber(ccNumber);
cc.setType(ccType);
cc.setExpMonth(...);
cc.setExpYear(...);

User user = (User) session.get(User.class, userId);

// Call convenience method that sets both sides of the association
user.addBillingDetails(cc);

// Complete unit of work

As usual, addBillingDetails() calls getBillingDetails().add(cc) and cc.setUser(this) to guarantee the integrity of the relationship by setting both pointers.

You may iterate over the collection and handle instances of CreditCard and CheckingAccount polymorphically (you probably don't want to bill users several times in the final system, though):

User user = (User) session.get(User.class, userId);

for( BillingDetails bd : user.getBillingDetails() ) {
    // Invoke CreditCard.pay() or BankAccount.pay()
    bd.pay(paymentAmount);
}

In the examples so far, we assumed that BillingDetails is a class mapped explicitly and that the inheritance mapping strategy is table per class hierarchy, or normalized with table per subclass.

However, if the hierarchy is mapped with table per concrete class (implicit polymorphism) or explicitly with table per concrete class with union, this scenario requires a more sophisticated solution.

7.3.3. Polymorphic associations to unions

Hibernate supports the polymorphic many-to-one and one-to-many associations shown in the previous sections even if a class hierarchy is mapped with the table per concrete class strategy. You may wonder how this works, because you may not have a table for the superclass with this strategy; if so, you can't reference or add a foreign key column to BILLING_DETAILS.

Review our discussion of table per concrete class with union in chapter 5, section 5.1.2, "Table per concrete class with unions." Pay extra attention to the polymorphic query Hibernate executes when retrieving instances of BillingDetails. Now, consider the following collection of BillingDetails mapped for User:

<set name="billingDetails"
     inverse="true">
    <key column="USER_ID"/>
    <one-to-many class="BillingDetails"/>
</set>

If you want to enable the polymorphic union feature, a requirement for this polymorphic association is that it's inverse; there must be a mapping on the opposite side. In the mapping of BillingDetails, with <union-subclass>, you have to include a <many-to-one> association:

<class name="BillingDetails" abstract="true">

    <id name="id" column="BILLING_DETAILS_ID" .../>

    <property .../>

    <many-to-one name="user"
                 column="USER_ID"
                 class="User"/>

    <union-subclass name="CreditCard" table="CREDIT_CARD">
        <property .../>
    </union-subclass>

    <union-subclass name="BankAccount" table="BANK_ACCOUNT">
        <property .../>
    </union-subclass>

</class>

You have two tables for both concrete classes of the hierarchy. Each table has a foreign key column, USER_ID, referencing the USERS table. The schema is shown in figure 7.14.

Now, consider the following data-access code:

aUser.getBillingDetails().iterator().next();

Figure 7-14. Two concrete classes mapped to two separate tables

Hibernate executes a UNION query to retrieve all instances that are referenced in this collection:

select
    BD.*
from
  ( select
      BILLING_DETAILS_ID, USER_ID, OWNER,
      NUMBER, EXP_MONTH, EXP_YEAR,
      null as ACCOUNT, null as BANKNAME, null as SWIFT,
      1 as CLAZZ
    from
      CREDIT_CARD

    union

    select
      BILLING_DETAILS_ID, USER_ID, OWNER,
      null as NUMBER, null as EXP_MONTH, null as EXP_YEAR
      ACCOUNT, BANKNAME, SWIFT,
      2 as CLAZZ
    from
      BANK_ACCOUNT
   ) BD
where
    BD.USER_ID = ?

The FROM-clause subselect is a union of all concrete class tables, and it includes the USER_ID foreign key values for all instances. The outer select now includes a restriction in the WHERE clause to all rows referencing a particular user.

This magic works great for retrieval of data. If you manipulate the collection and association, the noninverse side is used to update the USER_ID column(s) in the concrete table. In other words, the modification of the inverse collection has no effect: The value of the user property of a CreditCard or BankAccount instance is taken.

Now consider the many-to-one association defaultBillingDetails again, mapped with the DEFAULT_BILLING_DETAILS_ID column in the USERS table. Hibernate executes a UNION query that looks similar to the previous query to retrieve this instance, if you access the property. However, instead of a restriction in the WHERE clause to a particular user, the restriction is made on a particular BILLING_DETAILS_ID.

Important: Hibernate cannot and will not create a foreign key constraint for DEFAULT_BILLING_DETAILS_ID with this strategy. The target table of this reference can be any of the concrete tables, which can't be constrained easily. You should consider writing a custom integrity rule for this column with a database trigger.

One problematic inheritance strategy remains: table per concrete class with implicit polymorphism.

7.3.4. Polymorphic table per concrete class

In chapter 5, section 5.1.1, "Table per concrete class with implicit polymorphism," we defined the table per concrete class mapping strategy and observed that this mapping strategy makes it difficult to represent a polymorphic association, because you can't map a foreign key relationship to a table of the abstract superclass. There is no table for the superclass with this strategy; you have tables only for concrete classes. You also can't create a UNION, because Hibernate doesn't know what unifies the concrete classes; the superclass (or interface) isn't mapped anywhere.

Hibernate doesn't support a polymorphic billingDetails one-to-many collection in User, if this inheritance mapping strategy is applied on the BillingDetails hierarchy. If you need polymorphic many-to-one associations with this strategy, you'll have to resort to a hack. The technique we'll show you in this section should be your last choice. Try to switch to a <union-subclass> mapping first.

Suppose that you want to represent a polymorphic many-to-one association from User to BillingDetails, where the BillingDetails class hierarchy is mapped with a table per concrete class strategy and implicit polymorphic behavior in Hibernate. You have a CREDIT_CARD table and a BANK_ACCOUNT table, but no BILLING_DETAILS table. Hibernate needs two pieces of information in the USERS table to uniquely identify the associated default CreditCard or BankAccount:

  • The name of the table in which the associated instance resides

  • The identifier of the associated instance

The USERS table requires a DEFAULT_BILLING_DETAILS_TYPE column in addition to the DEFAULT_BILLING_DETAILS_ID. This extra column works as an additional discriminator and requires a Hibernate <any> mapping in User.hbm.xml:

<any name="defaultBillingDetails"
     id-type="long"
     meta-type="string">
    <meta-value value="CREDIT_CARD" class="CreditCard"/>
    <meta-value value="BANK_ACCOUNT" class="BankAccount"/>
    <column name="DEFAULT_BILLING_DETAILS_TYPE"/>
    <column name="DEFAULT_BILLING_DETAILS_ID"/>
</any>

The meta-type attribute specifies the Hibernate type of the DEFAULT_BILLING_DETAILS_TYPE column; the id-type attribute specifies the type of the DEFAULT_BILLING_DETAILS_IDcolumn (it's necessary for CreditCard and BankAccount to have the same identifier type).

The <meta-value> elements tell Hibernate how to interpret the value of the DEFAULT_BILLING_DETAILS_TYPE column. You don't need to use the full table name here—you can use any value you like as a type discriminator. For example, you can encode the information in two characters:

<any name="defaultBillingDetails"
     id-type="long"
     meta-type="string">
    <meta-value value="CC" class="CreditCard"/>
    <meta-value value="CA" class="BankAccount"/>
    <column name="DEFAULT_BILLING_DETAILS_TYPE"/>
    <column name="DEFAULT_BILLING_DETAILS_ID"/>
</any>

An example of this table structure is shown in figure 7.15.

Here is the first major problem with this kind of association: You can't add a foreign key constraint to the DEFAULT_BILLING_DETAILS_ID column, because some values refer to the BANK_ACCOUNT table and others to the CREDIT_CARD table. Thus, you need to come up with some other way to ensure integrity (a trigger, for example). This is the same issue you'd face with a <union-subclass> strategy.

Furthermore, it's difficult to write SQL table joins for this association. In particular, the Hibernate query facilities don't support this kind of association mapping, nor may this association be fetched using an outer join. We discourage the use of <any> associations for all but the most special cases. Also note that this mapping technique isn't available with annotations or in Java Persistence (this mapping is so rare that nobody asked for annotation support so far).

Figure 7-15. Using a discriminator column with an any association

As you can see, as long as you don't plan to create an association to a class hierarchy mapped with implicit polymorphism, associations are straightforward; you don't usually need to think about it. You may be surprised that we didn't show any JPA or annotation example in the previous sections—the runtime behavior is the same, and you don't need any extra mapping to get it.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.248.53