web-dev-qa-db-fra.com

Créez l'entité JPA parfaite

Je travaille avec JPA (implémentation Hibernate) depuis un certain temps et chaque fois que je dois créer des entités, je me trouve aux prises avec des problèmes tels que AccessType, propriétés immuables, equals/hashCode, ....
J'ai donc décidé d'essayer de trouver la meilleure pratique générale pour chaque problème et de l'écrire pour un usage personnel.
Cela ne me dérangerait cependant pas que quiconque me le dise ou me dise où je me trompe.

Classe d'entité

  • implémenter Serializable

    Raison: La spécification indique que vous devez le faire, mais certains fournisseurs JPA ne l'appliquent pas. Hibernate en tant que fournisseur JPA ne l'applique pas, mais il peut échouer quelque part au fond de son estomac avec ClassCastException, si Serializable n'a pas été implémenté.

Constructeurs

  • créer un constructeur avec tous les champs obligatoires de l'entité

    Raison: un constructeur doit toujours laisser l'instance créée dans un état sain.

  • en plus de ce constructeur: avoir un constructeur de paquet privé par défaut

    Raison: Le constructeur par défaut doit obliger Hibernate à initialiser l'entité. private est autorisé mais la visibilité du paquet privé (ou public) est requise pour la génération de proxy à l'exécution et la récupération efficace des données sans instrumentation bytecode.

Champs/Propriétés

  • Utiliser l'accès aux champs en général et l'accès aux propriétés en cas de besoin

    Raison: c'est probablement la question la plus discutable, car il n'y a pas d'argument clair et convaincant pour l'un ou l'autre (accès de propriété vs accès de champ); Cependant, l'accès aux champs semble être la solution préférée en raison d'un code plus clair, d'une meilleure encapsulation et de la nécessité de créer des paramètres pour les champs immuables

  • Omettre les setters pour les champs immuables (non requis pour le champ de type d'accès)

  • les propriétés peuvent être privées
    Raison: j'ai déjà entendu dire que protéger est préférable pour les performances (Hibernate), mais tout ce que je peux trouver sur le Web est: Hibernate peut également accéder aux méthodes d'accès, qu'elles soient publiques, privées ou protégées. comme champs publics, privés et protégés directement. Le choix vous appartient et vous pouvez l'adapter à la conception de votre application.

Est égal à/hashCode

  • Ne jamais utiliser un identifiant généré si cet identifiant n'est défini que lors de la persistance de l'entité
  • De préférence: utilisez des valeurs immuables pour former une clé d'entreprise unique et utilisez-la pour tester l'égalité
  • si une clé d'entreprise unique n'est pas disponible, utilisez un élément non transitoire UUID créé lors de l'initialisation de l'entité; Voir ce bel article pour plus d'informations.
  • jamais fait référence à des entités liées (ManyToOne); si cette entité (comme une entité mère) doit faire partie de la clé d'entreprise, comparez uniquement l'ID. L'appel de getId () sur un proxy ne déclenchera pas le chargement de l'entité, tant que vous utilisez type d'accès à la propriété .

Exemple d'entité

@Entity
@Table(name = "ROOM")
public class Room implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    @Column(name = "room_id")
    private Integer id;

    @Column(name = "number") 
    private String number; //immutable

    @Column(name = "capacity")
    private Integer capacity;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "building_id")
    private Building building; //immutable

    Room() {
        // default constructor
    }

    public Room(Building building, String number) {
        // constructor with required field
        notNull(building, "Method called with null parameter (application)");
        notNull(number, "Method called with null parameter (name)");

        this.building = building;
        this.number = number;
    }

    @Override
    public boolean equals(final Object otherObj) {
        if ((otherObj == null) || !(otherObj instanceof Room)) {
            return false;
        }
        // a room can be uniquely identified by it's number and the building it belongs to; normally I would use a UUID in any case but this is just to illustrate the usage of getId()
        final Room other = (Room) otherObj;
        return new EqualsBuilder().append(getNumber(), other.getNumber())
                .append(getBuilding().getId(), other.getBuilding().getId())
                .isEquals();
        //this assumes that Building.id is annotated with @Access(value = AccessType.PROPERTY) 
    }

    public Building getBuilding() {
        return building;
    }


    public Integer getId() {
        return id;
    }

    public String getNumber() {
        return number;
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(getNumber()).append(getBuilding().getId()).toHashCode();
    }

    public void setCapacity(Integer capacity) {
        this.capacity = capacity;
    }

    //no setters for number, building nor id

}

D'autres suggestions à ajouter à cette liste sont plus que bienvenues ...

UPDATE

Depuis la lecture de cet article j'ai adapté ma façon de mettre en œuvre eq/hC:

  • si une clé commerciale simple immuable est disponible: utilisez-la
  • dans tous les autres cas: utiliser un uuid
405
Stijn Geukens

Le spécification JPA 2. indique que:

  • La classe d'entité doit avoir un constructeur no-arg. Il peut aussi avoir d'autres constructeurs. Le constructeur no-arg doit être public ou protégé.
  • La classe d'entité doit être une classe de niveau supérieur. Une énumération ou une interface ne doit pas être désignée comme une entité.
  • La classe d'entité ne doit pas être finale. Aucune méthode ou variable d'instance persistante de la classe d'entité ne peut être finale.
  • Si une instance d'entité doit être transmise par la valeur en tant qu'objet détaché (par exemple, via une interface distante), la classe d'entités doit implémenter l'interface Serializable.
  • Les classes abstraites et concrètes peuvent être des entités. Les entités peuvent étendre les classes non-entités ainsi que les classes d'entités, et les classes non-entités peuvent étendre les classes d'entités.

La spécification ne contient aucune exigence concernant l'implémentation des méthodes equals et hashCode pour les entités, uniquement pour les classes de clé primaire et les clés de mappe, pour autant que je sache.

141
Edwin Dalorzo

Je vais essayer de répondre à plusieurs points clés: il s’agit d’une longue expérience de Hibernate/persistence incluant plusieurs applications majeures.

Classe d'entité: implémenter Serializable?

Les clés doivent implémenter Serializable. Les éléments qui vont être dans la session HttpSession ou qui seront envoyés sur le réseau par RPC/Java EE doivent implémenter Serializable. Autres choses: pas tellement. Passez votre temps sur ce qui est important.

Constructeurs: créer un constructeur avec tous les champs obligatoires de l'entité?

Le ou les constructeurs pour la logique d'application ne doivent comporter que quelques champs critiques de "clé étrangère" ou de "type/type" qui seront toujours connus lors de la création de l'entité. Le reste devrait être défini en appelant les méthodes setter - c’est à quoi elles servent.

Évitez de mettre trop de champs dans les constructeurs. Les constructeurs doivent être pratiques et donner à l’objet une bonne santé mentale. Nom, Type et/ou Parents sont généralement utiles.

OTOH si les règles d'application (aujourd'hui) exigent qu'un client possède une adresse, laissez-la à un utilisateur. C'est un exemple de "règle faible". Peut-être voudrez-vous créer un objet Client la semaine prochaine avant de passer à l'écran Saisir les détails? Ne vous trompez pas, laissez la possibilité pour des données inconnues, incomplètes ou "partiellement saisies".

Constructeurs: également, package constructeur privé par défaut?

Oui, mais utilisez 'protected' plutôt que le paquet private. Le sous-classement est une vraie douleur quand les internes nécessaires ne sont pas visibles.

Champs/Propriétés

Utilisez l'accès au champ 'propriété' pour Hibernate et en dehors de l'instance. Dans l'instance, utilisez directement les champs. Raison: permet à la réflexion standard, la méthode la plus simple et la plus fondamentale pour Hibernate, de fonctionner.

En ce qui concerne les champs "immuables" pour l'application - Hibernate doit encore pouvoir les charger. Vous pouvez essayer de rendre ces méthodes "privées" et/ou leur ajouter une annotation afin d'empêcher le code de l'application de créer des accès indésirables.

Remarque: lorsque vous écrivez une fonction equals (), utilisez les getters pour obtenir les valeurs de l'instance 'other'! Sinon, vous frapperez des champs non initialisés/vides sur des instances de proxy.

Protégé est meilleur pour les performances (Hibernate)?

Peu probable.

Equals/HashCode?

Ceci est pertinent pour travailler avec des entités, avant qu'elles aient été sauvegardées - ce qui est une question épineuse. Hashing/comparaison sur des valeurs immuables? Dans la plupart des applications métier, il n'y en a pas.

Un client peut changer d’adresse, changer le nom de son entreprise, etc. - ce n’est pas courant, mais cela arrive. Des corrections doivent également être possibles lorsque les données n'ont pas été entrées correctement.

Le peu de choses qui restent normalement immuables, sont Parenting et peut-être Type/Kind - normalement, l'utilisateur recrée l'enregistrement, plutôt que de le changer. Mais ceux-ci n'identifient pas uniquement l'entité!

Donc, longues et courtes, les données "immuables" revendiquées ne le sont pas vraiment. Les champs de clé primaire/ID sont générés dans le but précis de fournir une telle stabilité et immuabilité garanties.

Vous devez planifier et prendre en compte votre besoin de phases de travail de comparaison et de hachage et de traitement des demandes lorsque A) vous utilisez des "données liées/modifiées" à partir de l'interface utilisateur si vous comparez/un hachage sur des "champs rarement modifiés" ou B) qui utilisent " données non sauvegardées ", si vous comparez/hachage sur ID.

Equals/HashCode - si une clé d'entreprise unique n'est pas disponible, utilisez un UUID non transitoire créé lors de l'initialisation de l'entité

Oui, c'est une bonne stratégie lorsque cela est nécessaire. Sachez que les UUID ne sont pas libres, en termes de performances - et la mise en cluster complique les choses.

Equals/HashCode - ne fait jamais référence à des entités associées

"Si une entité associée (telle qu'une entité parente) doit faire partie de la clé d'entreprise, ajoutez un champ non insérable et non modifiable pour stocker l'ID parent (avec le même nom que le JoinColumn de ManytoOne) et utilisez-le dans le contrôle d'égalité. "

Cela semble être un bon conseil.

J'espère que cela t'aides!

67
Thomas W

Voici mon ajout de 2 cents aux réponses:

  1. En ce qui concerne l'accès aux champs ou aux propriétés (en dehors des considérations de performances), les deux sont légitimement accessibles à l'aide de getters et de setters. Ainsi, ma logique de modèle peut les définir/les obtenir de la même manière. La différence intervient lorsque le fournisseur d'exécution de la persistance (Hibernate, EclipseLink ou autre) doit persister/définir un enregistrement de la table A comportant une clé étrangère faisant référence à une colonne de la table B. Dans le cas d'un type d'accès de propriété, la persistance Le système d’exécution utilise ma méthode de définition codée pour attribuer une nouvelle valeur à la cellule de la colonne du tableau B. Dans le cas d'un type d'accès Field, le système d'exécution de persistance définit directement la cellule dans la colonne Table B. Cette différence n’a pas d’importance dans le contexte d’une relation unidirectionnelle, mais c’est une obligation d’utiliser ma propre méthode de définition codée (type d’accès de propriété) pour une relation bidirectionnelle, à condition que la méthode de définition soit bien conçue pour rendre compte de la cohérence. . La cohérence est un problème critique pour les relations bidirectionnelles. Référez-vous à ceci lien pour obtenir un exemple simple pour un passeur bien conçu.

  2. En référence à Equals/hashCode: Il est impossible d’utiliser les méthodes Equals/hashCode générées automatiquement par Eclipse pour les entités participant à une relation bidirectionnelle, sinon elles auront une référence circulaire, ce qui entraînera une exception de débordement de pile. Une fois que vous avez essayé une relation bidirectionnelle (par exemple OneToOne) et généré automatiquement Equals (), hashCode () ou même toString (), vous serez pris dans cette exception stackoverflow.

13
Sym-Sym

Interface d'entité

public interface Entity<I> extends Serializable {

/**
 * @return entity identity
 */
I getId();

/**
 * @return HashCode of entity identity
 */
int identityHashCode();

/**
 * @param other
 *            Other entity
 * @return true if identities of entities are equal
 */
boolean identityEquals(Entity<?> other);
}

Implémentation de base pour toutes les entités, simplifie l'implémentation d'Equal/Hashcode:

public abstract class AbstractEntity<I> implements Entity<I> {

@Override
public final boolean identityEquals(Entity<?> other) {
    if (getId() == null) {
        return false;
    }
    return getId().equals(other.getId());
}

@Override
public final int identityHashCode() {
    return new HashCodeBuilder().append(this.getId()).toHashCode();
}

@Override
public final int hashCode() {
    return identityHashCode();
}

@Override
public final boolean equals(final Object o) {
    if (this == o) {
        return true;
    }
    if ((o == null) || (getClass() != o.getClass())) {
        return false;
    }

    return identityEquals((Entity<?>) o);
}

@Override
public String toString() {
    return getClass().getSimpleName() + ": " + identity();
    // OR 
    // return ReflectionToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
}

Entité de salle impl:

@Entity
@Table(name = "ROOM")
public class Room extends AbstractEntity<Integer> {

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "room_id")
private Integer id;

@Column(name = "number") 
private String number; //immutable

@Column(name = "capacity")
private Integer capacity;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "building_id")
private Building building; //immutable

Room() {
    // default constructor
}

public Room(Building building, String number) {
    // constructor with required field
    notNull(building, "Method called with null parameter (application)");
    notNull(number, "Method called with null parameter (name)");

    this.building = building;
    this.number = number;
}

public Integer getId(){
    return id;
}

public Building getBuilding() {
    return building;
}

public String getNumber() {
    return number;
}


public void setCapacity(Integer capacity) {
    this.capacity = capacity;
}

//no setters for number, building nor id
}

Je ne vois pas l'intérêt de comparer l'égalité des entités en fonction des domaines d'activité dans chaque cas d'entités JPA. Cela pourrait être davantage le cas si ces entités JPA étaient considérées comme des ValueObjects gérés par le domaine, au lieu d’entités contrôlées par le domaine (pour lesquelles ces exemples de code sont destinés).

9
ahaaman