Hibernate.orgCommunity Documentation

Chapitre 4. Classes persistantes

4.1. Un exemple simple de POJO
4.1.1. Implémenter un constructeur sans argument
4.1.2. Fournir une propriété d'identifiant (optionnel)
4.1.3. Favoriser les classes non finales (optionnel)
4.1.4. Déclarer les accesseurs et mutateurs des attributs persistants (optionnel)
4.2. Implémenter l'héritage
4.3. Implémenter equals() et hashCode()
4.4. Modèles dynamiques
4.5. Tuplizers
4.6. EntityNameResolvers

Les classes persistantes sont les classes d'une application qui implémentent les entités d'un problème métier (par ex. Client et Commande dans une application de commerce électronique). Toutes les instances d'une classe persistante ne sont pas forcément dans l'état persistant - en revanche, une instance peut être éphémère (transient) ou détachée.

Hibernate fonctionne de manière optimale lorsque ces classes suivent quelques règles simples, aussi connues comme le modèle de programmation Plain Old Java Object (POJO). Cependant, aucune de ces règles ne sont des besoins absolus. En effet, Hibernate3 présuppose très peu de choses à propos de la nature de vos objets persistants. Vous pouvez exprimer un modèle de domaine par d'autres moyens : utiliser des arbres d'instances de Map, par exemple.

Toute bonne application Java nécessite une classe persistante représentant les félins. Par exemple :

package eg;

import java.util.Set;
import java.util.Date;
public class Cat {
    private Long id; // identifier
    private Date birthdate;
    private Color color;
    private char sex;
    private float weight;
    private int litterId;
    private Cat mother;
    private Set kittens = new HashSet();
    private void setId(Long id) {
        this.id=id;
    }
    public Long getId() {
        return id;
    }
    void setBirthdate(Date date) {
        birthdate = date;
    }
    public Date getBirthdate() {
        return birthdate;
    }
    void setWeight(float weight) {
        this.weight = weight;
    }
    public float getWeight() {
        return weight;
    }
    public Color getColor() {
        return color;
    }
    void setColor(Color color) {
        this.color = color;
    }
    void setSex(char sex) {
        this.sex=sex;
    }
    public char getSex() {
        return sex;
    }
    void setLitterId(int id) {
        this.litterId = id;
    }
    public int getLitterId() {
        return litterId;
    }
    void setMother(Cat mother) {
        this.mother = mother;
    }
    public Cat getMother() {
        return mother;
    }
    void setKittens(Set kittens) {
        this.kittens = kittens;
    }
    public Set getKittens() {
        return kittens;
    }
    
    // addKitten not needed by Hibernate
    public void addKitten(Cat kitten) {
            kitten.setMother(this);
        kitten.setLitterId( kittens.size() ); 
        kittens.add(kitten);
    }
}

On explore quatre règles principales de classes persistantes en détail dans les sections qui suivent :

Une sous-classe doit également suivre la première et la seconde règle. Elle hérite sa propriété d'identifiant de la classe mère Cat. Par exemple :

package eg;


public class DomesticCat extends Cat {
        private String name;
        public String getName() {
                return name;
        }
        protected void setName(String name) {
                this.name=name;
        }
}

Vous devez surcharger les méthodes equals() et hashCode() si vous :

Hibernate garantit l'équivalence de l'identité persistante (ligne de base de données) et l'identité Java seulement à l'intérieur de la portée d'une session particulière. Donc dès que nous mélangeons des instances venant de différentes sessions, nous devons implémenter equals() et hashCode() si nous souhaitons avoir une sémantique correcte pour les Set s.

La manière la plus évidente est d'implémenter equals()/hashCode() en comparant la valeur de l'identifiant des deux objets. Si cette valeur est identique, les deux doivent représenter la même ligne de base de données, ils sont donc égaux (si les deux sont ajoutés à un Set, nous n'aurons qu'un seul élément dans le Set). Malheureusement, nous ne pouvons pas utiliser cette approche avec des identifiants générés ! Hibernate n'assignera de valeur d'identifiant qu'aux objets qui sont persistants, une instance nouvellement créée n'aura donc pas de valeur d'identifiant ! De plus, si une instance est non sauvegardée et actuellement dans un Set, le sauvegarder assignera une valeur d'identifiant à l'objet. Si equals() et hashCode() sont basées sur la valeur de l'identifiant, le code de hachage devrait changer, rompant le contrat du Set. Consultez le site web de Hibernate pour des informations plus approfondies. Notez que ceci n'est pas un problème Hibernate, mais concerne la sémantique normale de Java pour l'identité et l'égalité d'un objet.

Nous recommandons donc d'implémenter equals() et hashCode() en utilisant l'égalité par clé métier. L'égalité par clé métier signifie que la méthode equals() compare uniquement les propriétés qui forment une clé métier, une clé qui identifierait notre instance dans le monde réel (une clé candidate naturelle) :

public class Cat {


    ...
    public boolean equals(Object other) {
        if (this == other) return true;
        if ( !(other instanceof Cat) ) return false;
        final Cat cat = (Cat) other;
        if ( !cat.getLitterId().equals( getLitterId() ) ) return false;
        if ( !cat.getMother().equals( getMother() ) ) return false;
        return true;
    }
    public int hashCode() {
        int result;
        result = getMother().hashCode();
        result = 29 * result + getLitterId();
        return result;
    }
}

A business key does not have to be as solid as a database primary key candidate (see Section 12.1.3, « L'identité des objets »). Immutable or unique properties are usually good candidates for a business key.

Les entités persistantes ne doivent pas nécessairement être représentées comme des classes POJO ou des objets JavaBean à l'exécution. Hibernate supporte aussi les modèles dynamiques (en utilisant des Map s de Map s à l'exécution) et la représentation des entités comme des arbres DOM4J. Avec cette approche, vous n'écrivez pas de classes persistantes, seulement des fichiers de mappage.

By default, Hibernate works in normal POJO mode. You can set a default entity representation mode for a particular SessionFactory using the default_entity_mode configuration option (see Tableau 3.3, « Propriétés de configuration Hibernate »).

Les exemples suivants démontrent la représentation utilisant des Map s. D'abord, dans le fichier de mappage, un entity-name doit être déclaré au lieu (ou en plus) d'un nom de classe :


<hibernate-mapping>

    <class entity-name="Customer">

        <id name="id"
            type="long"
            column="ID">
            <generator class="sequence"/>
        </id>

        <property name="name"
            column="NAME"
            type="string"/>

        <property name="address"
            column="ADDRESS"
            type="string"/>

        <many-to-one name="organization"
            column="ORGANIZATION_ID"
            class="Organization"/>

        <bag name="orders"
            inverse="true"
            lazy="false"
            cascade="all">
            <key column="CUSTOMER_ID"/>
            <one-to-many class="Order"/>
        </bag>

    </class>
    
</hibernate-mapping
>

Notez que même si des associations sont déclarées en utilisant des noms de classe cible, le type de cible d'une association peut aussi être une entité dynamique au lieu d'un POJO.

Après avoir configuré le mode d'entité par défaut à dynamic-map pour la SessionFactory, nous pouvons lors de l'exécution fonctionner avec des Map s de Map s :

Session s = openSession();

Transaction tx = s.beginTransaction();
// Create a customer
Map david = new HashMap();
david.put("name", "David");
// Create an organization
Map foobar = new HashMap();
foobar.put("name", "Foobar Inc.");
// Link both
david.put("organization", foobar);
// Save both
s.save("Customer", david);
s.save("Organization", foobar);
tx.commit();
s.close();

Les avantages d'un mappage dynamique sont un gain de temps pour le prototypage sans la nécessité d'implémenter les classes d'entité. Pourtant, vous perdez la vérification du typage au moment de la compilation et aurez plus d'exceptions à gérer lors de l'exécution. Grâce au mappage de Hibernate, le schéma de la base de données peut facilement être normalisé et solidifié, permettant de rajouter une implémentation propre du modèle de domaine plus tard.

Les modes de représentation d'une entité peuvent aussi être configurés en se basant sur Session :

Session dynamicSession = pojoSession.getSession(EntityMode.MAP);


// Create a customer
Map david = new HashMap();
david.put("name", "David");
dynamicSession.save("Customer", david);
...
dynamicSession.flush();
dynamicSession.close()
...
// Continue on pojoSession

Veuillez noter que l'appel à getSession() en utilisant un EntityMode se fait sur l'API Session, et non sur SessionFactory. De cette manière, la nouvelle Session partage les connexions JDBC, transactions et autres informations de contexte sous-jacentes. Cela signifie que vous n'avez pas à appeler flush() et close() sur la Session secondaire, et laissez aussi la gestion de la transaction et de la connexion à l'unité de travail primaire.

More information about the XML representation capabilities can be found in Chapitre 19, Mappage XML.

org.hibernate.tuple.Tuplizer, et ses sous-interfaces, sont responsables de la gestion d'une représentation particulière d'un fragment de données, en fonction du org.hibernate.EntityMode de représentation. Si un fragment donné de données est considéré comme une structure de données, alors un tuplizer sait comment créer une telle structure de données, comment extraire des valeurs et injecter des valeurs dans une telle structure de données. Par exemple, pour le mode d'entité POJO, le tuplizer correspondant sait comment créer le POJO à travers son constructeur et comment accéder aux propriétés du POJO utilisant les accesseurs de la propriété définie.

Il y a deux types de Tuplizers de haut niveau, représentés par les interfaces org.hibernate.tuple.EntityTuplizer et org.hibernate.tuple.ComponentTuplizer. Les EntityTuplizer s sont responsables de la gestion des contrats mentionnés ci-dessus pour les entités, alors que les ComponentTuplizer s s'occupent des composants.

Les utilisateurs peuvent aussi brancher leurs propres tuplizers. Il vous faudra peut-être utiliser une implémentation de java.util.Map autre que java.util.HashMap dans le mode d'entité dynamic-map ; ou peut-être aurez vous besoin de définir une stratégie de génération de proxy différente de celle utilisée par défaut. Les deux devraient être effectuées en définissant une implémentation de tuplizer utilisateur. Les définitions de tuplizers sont attachées au mappage de l'entité ou du composant qu'ils devraient gérer. Revenons à l'exemple de notre entité utilisateur :


<hibernate-mapping>
    <class entity-name="Customer">
        <!--
            Override the dynamic-map entity-mode
            tuplizer for the customer entity
        -->
        <tuplizer entity-mode="dynamic-map"
                class="CustomMapTuplizerImpl"/>

        <id name="id" type="long" column="ID">
            <generator class="sequence"/>
        </id>

        <!-- other properties -->
        ...
    </class>
</hibernate-mapping>


public class CustomMapTuplizerImpl
        extends org.hibernate.tuple.entity.DynamicMapEntityTuplizer {
    // override the buildInstantiator() method to plug in our custom map...
    protected final Instantiator buildInstantiator(
            org.hibernate.mapping.PersistentClass mappingInfo) {
        return new CustomMapInstantiator( mappingInfo );
    }

    private static final class CustomMapInstantiator
            extends org.hibernate.tuple.DynamicMapInstantitor {
        // override the generateMap() method to return our custom map...
            protected final Map generateMap() {
                    return new CustomMap();
            }
    }
}

L'interface org.hibernate.EntityNameResolver représente un contrat pour résoudre le nom de l'entité d'une instance d'entité donnée. L'interface définit une méthode simple resolveEntityName, à qui l'on passe l'instance d'entité et qui doit retourner le nom d'entité qui convient (null est accepté et indiquerait que le resolver ne sait pas comment résoudre le nom de l'entité de l'instance d'entité donnée). Normalement, un org.hibernate.EntityNameResolver est surtout utile pour les modèles dynamiques. Vous pourriez, par exemple, utiliser des interfaces proxy comme modèle de domaine. La suite de test Hibernate comprend un exemple de ce style précis d'utilisation dans org.hibernate.test.dynamicentity.tuplizer2. Vous trouverez ci dessous une illustration du code de ce package.

/**

 * A very trivial JDK Proxy InvocationHandler implementation where we proxy an interface as
 * the domain model and simply store persistent state in an internal Map.  This is an extremely
 * trivial example meant only for illustration.
 */
public final class DataProxyHandler implements InvocationHandler {
        private String entityName;
        private HashMap data = new HashMap();
        public DataProxyHandler(String entityName, Serializable id) {
                this.entityName = entityName;
                data.put( "Id", id );
        }
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                String methodName = method.getName();
                if ( methodName.startsWith( "set" ) ) {
                        String propertyName = methodName.substring( 3 );
                        data.put( propertyName, args[0] );
                }
                else if ( methodName.startsWith( "get" ) ) {
                        String propertyName = methodName.substring( 3 );
                        return data.get( propertyName );
                }
                else if ( "toString".equals( methodName ) ) {
                        return entityName + "#" + data.get( "Id" );
                }
                else if ( "hashCode".equals( methodName ) ) {
                        return new Integer( this.hashCode() );
                }
                return null;
        }
        public String getEntityName() {
                return entityName;
        }
        public HashMap getData() {
                return data;
        }
}
/**
 *
 */
public class ProxyHelper {
    public static String extractEntityName(Object object) {
        // Our custom java.lang.reflect.Proxy instances actually bundle
        // their appropriate entity name, so we simply extract it from there
        // if this represents one of our proxies; otherwise, we return null
        if ( Proxy.isProxyClass( object.getClass() ) ) {
            InvocationHandler handler = Proxy.getInvocationHandler( object );
            if ( DataProxyHandler.class.isAssignableFrom( handler.getClass() ) ) {
                DataProxyHandler myHandler = ( DataProxyHandler ) handler;
                return myHandler.getEntityName();
            }
        }
        return null;
    }
    // various other utility methods ....
}
/**
 * The EntityNameResolver implementation.
 * IMPL NOTE : An EntityNameResolver really defines a strategy for how entity names should be
 * resolved.  Since this particular impl can handle resolution for all of our entities we want to
 * take advantage of the fact that SessionFactoryImpl keeps these in a Set so that we only ever
 * have one instance registered.  Why?  Well, when it comes time to resolve an entity name,
 * Hibernate must iterate over all the registered resolvers.  So keeping that number down
 * helps that process be as speedy as possible.  Hence the equals and hashCode impls
 */
public class MyEntityNameResolver implements EntityNameResolver {
    public static final MyEntityNameResolver INSTANCE = new MyEntityNameResolver();
    public String resolveEntityName(Object entity) {
        return ProxyHelper.extractEntityName( entity );
    }
    public boolean equals(Object obj) {
        return getClass().equals( obj.getClass() );
    }
    public int hashCode() {
        return getClass().hashCode();
    }
}
public class MyEntityTuplizer extends PojoEntityTuplizer {
        public MyEntityTuplizer(EntityMetamodel entityMetamodel, PersistentClass mappedEntity) {
                super( entityMetamodel, mappedEntity );
        }
        public EntityNameResolver[] getEntityNameResolvers() {
                return new EntityNameResolver[] { MyEntityNameResolver.INSTANCE };
        }
    public String determineConcreteSubclassEntityName(Object entityInstance, SessionFactoryImplementor factory) {
        String entityName = ProxyHelper.extractEntityName( entityInstance );
        if ( entityName == null ) {
            entityName = super.determineConcreteSubclassEntityName( entityInstance, factory );
        }
        return entityName;
    }
    ...
}
        

Pour enregistrer un org.hibernate.EntityNameResolver, les utilisateurs doivent soit :

  1. Implement a custom Tuplizer, implementing the getEntityNameResolvers method.

  2. L'enregistrer dans org.hibernate.impl.SessionFactoryImpl (qui est la classe d'implémentation de org.hibernate.SessionFactory) à l'aide de la méthode registerEntityNameResolver.