Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 87 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
87
Dung lượng
1,3 MB
Nội dung
228 CHAPTER 5 Inheritance and custom types You write a CompositeUserType if you need the full power of Hibernate que- ries. This (slightly more complex) interface exposes the properties of the MonetaryAmount to Hibernate queries. We’ll now map it again with this more flex- ible customization interface to two columns, effectively producing an equivalent to a component mapping. 5.3.5 Creating a CompositeUserType To demonstrate the flexibility of custom mappings types, you don’t change the MonetaryAmount class (and other persistent classes) at all—you change only the custom mapping type, as shown in listing 5.5. public class MonetaryAmountCompositeUserType implements CompositeUserType { // public int[] sqlTypes() public Class returnedClass public boolean isMutable public Object deepCopy public Serializable disassemble public Object assemble public Object replace public boolean equals public int hashCode public Object nullSafeGet(ResultSet resultSet, String[] names, SessionImplementor session, Object owner) throws SQLException { BigDecimal value = resultSet.getBigDecimal( names[0] ); if (resultSet.wasNull()) return null; Currency currency = Currency.getInstance(resultSet.getString( names[1] ) ); return new MonetaryAmount(value, currency); } public void nullSafeSet(PreparedStatement statement, Object value, int index, SessionImplementor session) throws SQLException { if (value==null) { statement.setNull(index, Hibernate.BIG_DECIMAL.sqlType()); statement.setNull(index+1, Hibernate.CURRENCY.sqlType()); } else { Listing 5.5 Custom mapping type for monetary amounts in new database schemas B C D Creating custom mapping types 229 MonetaryAmount amount = (MonetaryAmount) value; String currencyCode = amount.getCurrency().getCurrencyCode(); statement.setBigDecimal( index, amount.getAmount() ); statement.setString( index+1, currencyCode ); } } public String[] getPropertyNames() { return new String[] { "amount", "currency" }; } public Type[] getPropertyTypes() { return new Type[] { Hibernate.BIG_DECIMAL, Hibernate.CURRENCY }; } public Object getPropertyValue(Object component, int property) { MonetaryAmount monetaryAmount = (MonetaryAmount) component; if (property == 0) return monetaryAmount.getAmount(); else return monetaryAmount.getCurrency(); } public void setPropertyValue(Object component, int property, Object value) { throw new UnsupportedOperationException("Immutable MonetaryAmount!"); } } The CompositeUserType interface requires the same housekeeping methods as the UserType you created earlier. However, the sqlTypes() method is no longer needed. Loading a value now is straightforward: You transform two column values in the result set to two property values in a new MonetaryAmount instance. Saving a value involves setting two parameters on the prepared statement. A CompositeUserType exposes the properties of the value type through getProp- ertyNames() . The properties each have their own type, as defined by getPropertyTypes() . The types of the SQL columns are now implicit from this method. The getPropertyValue() method returns the value of an individual property of the MonetaryAmount . E F G H B C D E F G 230 CHAPTER 5 Inheritance and custom types The setPropertyValue() method sets the value of an individual property of the MonetaryAmount . The initialPrice property now maps to two columns, so you need to declare both in the mapping file. The first column stores the value; the second stores the currency of the MonetaryAmount : <property name="initialPrice" type="persistence.MonetaryAmountCompositeUserType"> <column name="INITIAL_PRICE"/> <column name="INITIAL_PRICE_CURRENCY"/> </property> If Item is mapped with annotations, you have to declare several columns for this property. You can’t use the javax.persistence.Column annotation several times, so a new, Hibernate-specific annotation is needed: @org.hibernate.annotations.Type( type = "persistence.MonetaryAmountUserType" ) @org.hibernate.annotations.Columns(columns = { @Column(name="INITIAL_PRICE"), @Column(name="INITIAL_PRICE_CURRENCY", length = 2) }) private MonetaryAmount initialPrice; In a Hibernate query, you can now refer to the amount and currency properties of the custom type, even though they don’t appear anywhere in the mapping docu- ment as individual properties: from Item i where i.initialPrice.amount > 100.0 and i.initialPrice.currency = 'AUD' You have extended the buffer between the Java object model and the SQL data- base schema with the new custom composite type. Both representations are now more robust to changes. Note that the number of columns isn’t relevant for your choice of UserType versus CompositeUserType —only your desire to expose value type properties for Hibernate queries. Parameterization is a helpful feature for all custom mapping types. 5.3.6 Parameterizing custom types Let’s assume that you face the initial problem again: conversion of money to a dif- ferent currency when storing it to the database. Often, problems are more subtle than a generic conversion; for example, you may store US dollars in some tables H Creating custom mapping types 231 and Euros in others. You still want to write a single custom mapping type for this, which can do arbitrary conversions. This is possible if you add the Parameter- izedType interface to your UserType or CompositeUserType classes: public class MonetaryAmountConversionType implements UserType, ParameterizedType { // Configuration parameter private Currency convertTo; public void setParameterValues(Properties parameters) { this.convertTo = Currency.getInstance( parameters.getProperty("convertTo") ); } // Housekeeping methods public Object nullSafeGet(ResultSet resultSet, String[] names, SessionImplementor session, Object owner) throws SQLException { BigDecimal value = resultSet.getBigDecimal( names[0] ); if (resultSet.wasNull()) return null; // When loading, take the currency from the database Currency currency = Currency.getInstance( resultSet.getString( names[1] ) ); return new MonetaryAmount(value, currency); } public void nullSafeSet(PreparedStatement statement, Object value, int index, SessionImplementor session) throws SQLException { if (value==null) { statement.setNull(index, Types.NUMERIC); } else { MonetaryAmount amount = (MonetaryAmount) value; // When storing, convert the amount to the // currency this converter was parameterized with MonetaryAmount dbAmount = MonetaryAmount.convert(amount, convertTo); statement.setBigDecimal( index, dbAmount.getAmount() ); statement.setString( index+1, dbAmount.getCurrencyCode() ); } } } 232 CHAPTER 5 Inheritance and custom types We left out the usual mandatory housekeeping methods in this example. The important additional method is setParameterValues() of the Parameterized- Type interface. Hibernate calls this method on startup to initialize this class with a convertTo parameter. The nullSafeSet() methods uses this setting to convert to the target currency when saving a MonetaryAmount . The nullSafeGet() method takes the currency that is present in the database and leaves it to the client to deal with the currency of a loaded MonetaryAmount (this asymmetric implementation isn’t the best idea, naturally). You now have to set the configuration parameters in your mapping file when you apply the custom mapping type. A simple solution is the nested <type> map- ping on a property: <property name="initialPrice"> <column name="INITIAL_PRICE"/> <column name="INITIAL_PRICE_CUR"/> <type name="persistence.MonetaryAmountConversionType"> <param name="convertTo">USD</param> </type> </property> However, this is inconvenient and requires duplication if you have many monetary amounts in your domain model. A better strategy uses a separate definition of the type, including all parameters, under a unique name that you can then reuse across all your mappings. You do this with a separate <typedef> , an element (you can also use it without parameters): <typedef class="persistence.MonetaryAmountConversionType" name="monetary_amount_usd"> <param name="convertTo">USD</param> </typedef> <typedef class="persistence.MonetaryAmountConversionType" name="monetary_amount_eur"> <param name="convertTo">EUR</param> </typedef> What we show here is a binding of a custom mapping type with some arguments to the names monetary_amount_usd and monetary_amount_eur . This definition can be placed anywhere in your mapping files; it’s a child element of <hibernate- mapping> (as mentioned earlier in the book, larger applications have often one or several MyCustomTypes.hbm.xml files with no class mappings). With Hibernate extensions, you can define named custom types with parameters in annotations: Creating custom mapping types 233 @org.hibernate.annotations.TypeDefs({ @org.hibernate.annotations.TypeDef( name="monetary_amount_usd", typeClass = persistence.MonetaryAmountConversionType.class, parameters = { @Parameter(name="convertTo", value="USD") } ), @org.hibernate.annotations.TypeDef( name="monetary_amount_eur", typeClass = persistence.MonetaryAmountConversionType.class, parameters = { @Parameter(name="convertTo", value="EUR") } ) }) This annotation metadata is global as well, so it can be placed outside any Java class declaration (right after the import statements) or in a separate file, pack- age-info.java , as discussed in chapter 2, section 2.2.1, “Using Hibernate Anno- tations.” A good location in this system is in a package-info.java file in the persistence package. In XML mapping files and annotation mappings, you now refer to the defined type name instead of the fully qualified class name of your custom type: <property name="initialPrice" type="monetary_amount_usd"> <column name="INITIAL_PRICE"/> <column name="INITIAL_PRICE_CUR"/> </property> @org.hibernate.annotations.Type(type = "monetary_amount_eur") @org.hibernate.annotations.Columns({ @Column(name = "BID_AMOUNT"), @Column(name = "BID_AMOUNT_CUR") }) private MonetaryAmount bidAmount; Let’s look at a different, extremely important, application of custom mapping types. The type-safe enumeration design pattern can be found in almost all appli- cations. 5.3.7 Mapping enumerations An enumeration type is a common Java idiom where a class has a constant (small) number of immutable instances. In CaveatEmptor, this can be applied to credit cards: for example, to express the possible types a user can enter and the applica- tion offers (Mastercard, Visa, and so on). Or, you can enumerate the possible rat- ings a user can submit in a Comment , about a particular auction. 234 CHAPTER 5 Inheritance and custom types In older JDKs, you had to implement such classes (let’s call them CreditCard- Type and Rating ) yourself, following the type-safe enumeration pattern. This is still the right way to do it if you don’t have JDK 5.0; the pattern and compatible custom mapping types can be found on the Hibernate community website. Using enumerations in JDK 5.0 If you use JDK 5.0, you can use the built-in language support for type-safe enumer- ations. For example, a Rating class looks as follows: package auction.model; public enum Rating { EXCELLENT, OK, BAD } The Comment class has a property of this type: public class Comment { private Rating rating; private Item auction; } This is how you use the enumeration in the application code: Comment goodComment = new Comment(Rating.EXCELLENT, thisAuction); You now have to persist this Comment instance and its Rating . One approach is to use the actual name of the enumeration and save it to a VARCHAR column in the COMMENTS table. This RATING column will then contain EXCELLENT , OK , or BAD , depending on the Rating given. Let’s write a Hibernate UserType that can load and store VARCHAR -backed enu- merations, such as the Rating . Writing a custom enumeration handler Instead of the most basic UserType interface, we now want to show you the EnhancedUserType interface. This interface allows you to work with the Comment entity in XML representation mode, not only as a POJO (see the discussion of data representations in chapter 3, section 3.4, “Alternative entity representa- tion”). Furthermore, the implementation you’ll write can support any VARCHAR - backed enumeration, not only Rating , thanks to the additional Parameterized- Type interface. Look at the code in listing 5.6. Creating custom mapping types 235 public class StringEnumUserType implements EnhancedUserType, ParameterizedType { private Class<Enum> enumClass; public void setParameterValues(Properties parameters) { String enumClassName = parameters.getProperty("enumClassname"); try { enumClass = ReflectHelper.classForName(enumClassName); } catch (ClassNotFoundException cnfe) { throw new HibernateException("Enum class not found", cnfe); } } public Class returnedClass() { return enumClass; } public int[] sqlTypes() { return new int[] { Hibernate.STRING.sqlType() }; } public boolean isMutable public Object deepCopy public Serializable disassemble public Object replace public Object assemble public boolean equals public int hashCode public Object fromXMLString(String xmlValue) { return Enum.valueOf(enumClass, xmlValue); } public String objectToSQLString(Object value) { return '\'' + ( (Enum) value ).name() + '\''; } public String toXMLString(Object value) { return ( (Enum) value ).name(); } public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws SQLException { String name = rs.getString( names[0] ); return rs.wasNull() ? null : Enum.valueOf(enumClass, name); } public void nullSafeSet(PreparedStatement st, Listing 5.6 Custom mapping type for string-backed enumerations B C D E F G H 236 CHAPTER 5 Inheritance and custom types Object value, int index) throws SQLException { if (value == null) { st.setNull(index, Hibernate.STRING.sqlType()); } else { st.setString( index, ( (Enum) value ).name() ); } } } The configuration parameter for this custom mapping type is the name of the enumeration class it’s used for, such as Rating . It’s also the class that is returned from this method. A single VARCHAR column is needed in the database table. You keep it portable by letting Hibernate decide the SQL datatype. These are the usual housekeeping methods for an immutable type. The following three methods are part of the EnhancedUserType and are used for XML marshalling. When you’re loading an enumeration, you get its name from the database and create an instance. When you’re saving an enumeration, you store its name. Next, you’ll map the rating property with this new custom type. Mapping enumerations with XML and annotations In the XML mapping, first create a custom type definition: <typedef class="persistence.StringEnumUserType" name="rating"> <param name="enumClassname">auction.model.Rating</param> </typedef> You can now use the type named rating in the Comment class mapping: <property name="rating" column="RATING" type="rating" not-null="true" update="false" access="field"/> B C D E F G H Creating custom mapping types 237 Because ratings are immutable, you map it as update="false" and enable direct field access (no setter method for immutable properties). If other classes besides Comment have a Rating property, use the defined custom mapping type again. The definition and declaration of this custom mapping type in annotations looks the same as the one you did in the previous section. On the other hand, you can rely on the Java Persistence provider to persist enumerations. If you have a property in one of your annotated entity classes of type java.lang.Enum (such as the rating in your Comment ), and it isn’t marked as @Transient or transient (the Java keyword), the Hibernate JPA implementation must persist this property out of the box without complaining; it has a built-in type that handles this. This built-in mapping type has to default to a representa- tion of an enumeration in the database. The two common choices are string rep- resentation, as you implemented for native Hibernate with a custom type, or ordinal representation. An ordinal representation saves the position of the selected enumeration option: for example, 1 for EXCELLENT , 2 for OK , and 3 for BAD . The database column also defaults to a numeric column. You can change this default enumeration mapping with the Enumerated annotation on your property: public class Comment { @Enumerated(EnumType.STRING) @Column(name = "RATING", nullable = false, updatable = false) private Rating rating; } You’ve now switched to a string-based representation, effectively the same repre- sentation your custom type can read and write. You can also use a JPA XML descriptor: <entity class="auction.model.Item" access="PROPERTY"> <attributes> <basic name="rating"> <column name="RATING" nullable="false" updatable="false"/> <enumerated>STRING</enumerated> </basic> </attributes> </entity> You may (rightfully) ask why you have to write your own custom mapping type for enumerations when obviously Hibernate, as a Java Persistence provider, can per- sist and load enumerations out of the box. The secret is that Hibernate Annota- tions includes several custom mapping types that implement the behavior defined [...]... typical Hibernate application ■ A java. util.SortedSet can be mapped with , and the sort attribute can be set to either a comparator or natural ordering for in-memory sorting Initialize the collection with a java. util.TreeSet instance ■ A java. util.List can be mapped with , preserving the position of each element with an additional index column in the collection table Initialize with a java. util.ArrayList... distinction, and how the Hibernate mapping type system works You used various built-in types and wrote your own custom types by utilizing the Hibernate extension points such as UserType and ParameterizedType Table 5.5 shows a summary you can use to compare native Hibernate features and Java Persistence Table 5.5 Hibernate and JPA comparison chart for chapter 5 Hibernate Core Java Persistence and EJB 3.0... API in Java Persistence for arbitrary and custom query parameters, so you have to fall back to the Hibernate Session API and create a Hibernate Query object We recommend that you become intimately familiar with the Hibernate type system and that you consider the creation of custom mapping types an essential skill—it will be useful in every application you develop with Hibernate or JPA Summary 5 .4 239... mapped with , preserving key and value pairs Use a java. util.HashMap to initialize a property Sets, bags, lists, and maps of value types 243 ■ A java. util.SortedMap can be mapped with element, and the sort attribute can be set to either a comparator or natural ordering for in-memory sorting Initialize the collection with a java. util.TreeMap instance ■ Arrays are supported by Hibernate with. .. supported by Hibernate, and it’s important that you use the right combination Hibernate only wraps the collection object you’ve already initialized on declaration of the field (or sometimes replaces it, if it’s not the right one) Without extending Hibernate, you can choose from the following collections: ■ A java. util.Set is mapped with a element Initialize the collection with a java. util.HashSet... collections of value types with annotations is different compared with mappings in XML; at the time of writing, it isn’t part of the Java Persistence standard but is available in Hibernate 6.3 Mapping collections with annotations The Hibernate Annotations package supports nonstandard annotations for the mapping of collections that contain value-typed elements, mainly org .hibernate. annotations.CollectionOfElements... the Java Collections framework doesn’t include a bag implementation However, the java. util.Collection interface has bag semantics, so you only need a matching implementation You have two choices: Sets, bags, lists, and maps of value types 245 ■ Write the collection property with the java. util.Collection interface, and, on declaration, initialize it with an ArrayList of the JDK Map the collection in Hibernate. .. collection in Hibernate with a standard or element Hibernate has a built-in PersistentBag that can deal with lists; however, consistent with the contract of a bag, it ignores the position of elements in the ArrayList In other words, you get a persistent Collection ■ Write the collection property with the java. util.List interface, and, on declaration, initialize it with an ArrayList of the... java. util.ArrayList ■ A java. util.Collection can be mapped with or Java doesn’t have a Bag interface or an implementation; however, java. util Collection allows bag semantics (possible duplicates, no element order is preserved) Hibernate supports persistent bags (it uses lists internally but ignores the index of the elements) Use a java. util.ArrayList to initialize a bag collection ■ A java. util.Map... individual properties of the embeddable component Note that @org .hibernate. annotations.MapKey is a more powerful replacement for @javax .persistence. MapKey, which isn’t very useful (see chapter 7, section 7.2 .4 “Mapping maps”) 6.3.2 Sorted and ordered collections A collection can also be sorted or ordered with Hibernate annotations: @org .hibernate. annotations.CollectionOfElements @JoinTable( name = "ITEM_IMAGE", . right one). Without extending Hibernate, you can choose from the following collections: ■ A java. util.Set is mapped with a <set> element. Initialize the collection with a java. util.HashSet Ini- tialize with a java. util.ArrayList . ■ A java. util.Collection can be mapped with <bag> or <idbag> . Java doesn’t have a Bag interface or an implementation; however, java. util. Collection . types 245 ■ Write the collection property with the java. util.Collection interface, and, on declaration, initialize it with an ArrayList of the JDK. Map the collection in Hibernate with a