Créez l'entité JPA parfaite [fermé]

422

Je travaille avec JPA (implémentation Hibernate) depuis un certain temps maintenant et chaque fois que j'ai besoin de créer des entités, je me retrouve avec des problèmes comme AccessType, les 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 commente ou me dise où je me trompe.

Classe d'entité

  • implémenter Serializable

    Raison: la spécification dit que vous devez le faire, mais certains fournisseurs JPA ne l'appliquent pas. Hibernate en tant que fournisseur JPA n'applique pas cela, 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 package constructeur par défaut privé

    Motif: le constructeur par défaut doit demander à Hibernate d'initialiser l'entité; private est autorisé mais la visibilité privée (ou publique) du package est requise pour la génération de proxy d'exécution et la récupération efficace des données sans instrumentation de bytecode.

Champs / Propriétés

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

    Raison: c'est probablement la question la plus discutable car il n'y a pas d'arguments clairs et convaincants pour l'un ou l'autre (accès à la propriété vs accès au champ); cependant, l'accès aux champs semble être le favori général 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 paramètres 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 entendu dire que protégé est meilleur pour les performances (mise en veille prolongée), mais tout ce que je peux trouver sur le Web est: Hibernate peut accéder aux méthodes d'accesseur publiques, privées et protégées, ainsi qu'aux champs publics, privés et protégés directement . Le choix vous appartient et vous pouvez l'adapter à la conception de votre application.

Equals / hashCode

  • N'utilisez jamais un identifiant généré si cet identifiant est uniquement défini lors de la persistance de l'entité
  • De préférence: utilisez des valeurs immuables pour former une clé métier unique et utilisez-la pour tester l'égalité
  • si une clé d'entreprise unique n'est pas disponible, utilisez un UUID non transitoire qui est créé lors de l'initialisation de l'entité; Consultez cet excellent article pour plus d'informations.
  • ne faites jamais référence à des entités liées (ManyToOne); si cette entité (comme une entité parente) doit faire partie de la clé d'entreprise, comparez uniquement les ID. L'appel de getId () sur un proxy ne déclenchera pas le chargement de l'entité, tant que vous utilisez le 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 ...

MISE À JOUR

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

  • si une clé métier simple immuable est disponible: utilisez-la
  • dans tous les autres cas: utilisez un uuid
Stijn Geukens
la source
6
Ce n'est pas une question, c'est une demande de révision avec une demande de liste. De plus, c'est très ouvert et vague, ou autrement dit: si une entité JPA est parfaite dépend de ce à quoi elle va être utilisée. Devrions-nous énumérer tout ce dont une entité pourrait avoir besoin dans toutes les utilisations possibles d'une entité?
meriton
Je sais que ce n'est pas une question claire pour laquelle je m'excuse. Ce n'est pas vraiment une demande de liste, mais plutôt une demande de commentaires / remarques bien que d'autres suggestions soient les bienvenues. N'hésitez pas à expliquer les utilisations possibles d'une entité JPA.
Stijn Geukens
Je voudrais aussi que les champs soient final(à en juger par votre omission des setters, je suppose que vous aussi).
Sridhar Sarnobat
Il faudrait l'essayer, mais je ne pense pas que final fonctionnera car Hibernate doit toujours être en mesure de définir les valeurs de ces propriétés.
Stijn Geukens
D'où notNullvient-il?
bruno

Réponses:

73

Je vais essayer de répondre à plusieurs points clés: cela vient d'une longue expérience de mise en veille prolongée / persistance, y compris plusieurs applications majeures.

Classe d'entité: implémenter Serializable?

Les clés doivent implémenter Serializable. Les trucs qui vont aller dans la session HttpSession, ou être envoyés sur le câble par RPC / Java EE, doivent implémenter Serializable. Autres trucs: 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 de la logique d'application ne devraient avoir que quelques champs "clé étrangère" ou "type / type" critiques qui seront toujours connus lors de la création de l'entité. Le reste doit être défini en appelant les méthodes de définition - c'est à cela qu'elles servent.

Évitez de mettre trop de champs dans les constructeurs. Les constructeurs doivent être pratiques et donner un bon sens de base à l'objet. Le nom, le type et / ou les parents sont généralement utiles.

OTOH si les règles d'application (aujourd'hui) exigent qu'un client ait une adresse, laissez-la à un passeur. C'est un exemple de "règle faible". La semaine prochaine, vous souhaitez peut-être créer un objet Client avant d'accéder à l'écran Entrer les détails? Ne vous trébuchez pas, laissez la possibilité de données inconnues, incomplètes ou "partiellement saisies".

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

Oui, mais utilisez «protégé» plutôt que package privé. Le sous-classement est un vrai problème lorsque les composants internes nécessaires ne sont pas visibles.

Champs / Propriétés

Utilisez l'accès au champ «propriété» pour Hibernate et depuis l'extérieur 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 basique d'Hibernate, de fonctionner.

Quant aux champs «immuables» à l'application - Hibernate doit toujours être capable de les charger. Vous pouvez essayer de rendre ces méthodes «privées» et / ou de mettre une annotation dessus, pour empêcher le code d'application de créer un accès indésirable.

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

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

Peu probable.

Equals / HashCode?

Cela est pertinent pour travailler avec des entités, avant qu'elles ne soient enregistrées - ce qui est un problème épineux. Hachage / comparaison sur des valeurs immuables? Dans la plupart des applications d'entreprise, il n'y en a pas.

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

Les quelques éléments qui restent normalement immuables sont la parentalité et peut-être le type / type - normalement, l'utilisateur recrée l'enregistrement plutôt que de les modifier. 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) travaillez avec des "données modifiées / liées" à partir de l'interface utilisateur si vous comparez / hachez sur des "champs rarement modifiés", ou B) travaillez avec " données non enregistrées ", si vous comparez / hachez sur l'ID.

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

Oui, c'est une bonne stratégie si nécessaire. Sachez que les UUID ne sont pas gratuits, en termes de performances, et que le clustering complique les choses.

Equals / HashCode - ne faites jamais référence à des entités liées

"Si une entité associée (comme une entité parent) doit faire partie de la clé métier, ajoutez un champ non insérable et non modifiable pour stocker l'ID parent (avec le même nom que ManytoOne JoinColumn) et utilisez cet identifiant dans la vérification d'égalité "

Cela ressemble à de bons conseils.

J'espère que cela t'aides!

Thomas W
la source
2
Re: constructeurs, je vois souvent zéro argument uniquement (c'est-à-dire aucun) et le code appelant a une longue liste de setters qui me semble un peu compliquée. Y a-t-il vraiment un problème avec quelques constructeurs qui répondent à vos besoins, ce qui rend le code appelant plus succinct?
Ouragan
totalement opiniâtre, spécialement sur ctor. quoi de plus beau code? un tas de ctors différents qui vous permet de savoir quelle (combinaison de) valeurs est nécessaire pour créer un état sain de l'obj ou un ctor sans argument qui ne donne aucune idée de ce qui doit être défini et dans quel ordre et le laisse sujet aux erreurs de l'utilisateur ?
mohamnag
1
@mohamnag Depends. Pour les données internes générées par le système, les beans strictement valides sont excellents; cependant, les applications commerciales modernes se composent d'un grand nombre d'écrans CRUD ou d'assistant de saisie de données utilisateur. Les données saisies par l'utilisateur sont souvent partiellement ou mal formées, du moins pendant la modification. Très souvent, il est même utile d'enregistrer un état incomplet pour un achèvement ultérieur - pensez à la capture des demandes d'assurance, aux inscriptions des clients, etc. Garder les contraintes au minimum (par exemple, clé primaire, clé commerciale et état) permet une plus grande flexibilité situations commerciales.
Thomas W
1
@ThomasW d'abord, je dois dire que je suis fortement convaincu par la conception basée sur le domaine et l'utilisation de noms pour les noms de classe et signifiant les verbes complets pour les méthodes. Dans ce paradigme, ce à quoi vous faites référence, ce sont en fait des DTO et non les entités de domaine qui devraient être utilisées pour le stockage temporaire des données. Ou vous venez de mal comprendre / structurer votre domaine.
mohamnag
@ThomasW lorsque je filtre toutes les phrases que vous essayez de dire que je suis novice, il n'y a aucune information dans votre commentaire, sauf en ce qui concerne la saisie des utilisateurs. Cette partie, comme je l'ai déjà dit, se fera dans les DTO et non directement dans l'entité. permet de parler dans 50 ans pour que vous deveniez 5% de ce grand esprit derrière DDD comme Fowler cheers: D
mohamnag
144

La spécification JPA 2.0 stipule que:

  • La classe d'entité doit avoir un constructeur sans argument. Il peut également avoir d'autres constructeurs. Le constructeur sans argument 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 définitive.
  • Si une instance d'entité doit être transmise par valeur en tant qu'objet détaché (par exemple, via une interface distante), la classe d'entité 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é ainsi que les classes d'entité, et les classes non-entité peuvent étendre les classes d'entité.

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és primaires et les clés de carte pour autant que je sache.

Edwin Dalorzo
la source
13
Certes, equals, hashcode, ... ne sont pas une exigence JPA mais sont bien sûr recommandés et considérés comme une bonne pratique.
Stijn Geukens
6
@TheStijn Eh bien, sauf si vous prévoyez de comparer des entités détachées pour l'égalité, cela n'est probablement pas nécessaire. Le gestionnaire d'entités est garanti de retourner la même instance d'une entité donnée chaque fois que vous la demandez. Donc, pour autant que je sache, vous pouvez très bien faire avec les comparaisons d'identité pour les entités gérées. Pourriez-vous, s'il vous plaît, élaborer un peu plus sur les scénarios dans lesquels vous considéreriez cela comme une bonne pratique?
Edwin Dalorzo
2
Je m'efforce d'avoir toujours une implémentation correcte de equals / hashCode. Non requis pour JPA mais je considère que c'est une bonne pratique lorsque des entités ou ajoutées à des ensembles. Vous pouvez décider de n'implémenter égal que lorsque des entités seront ajoutées aux ensembles, mais le savez-vous toujours à l'avance?
Stijn Geukens
10
@TheStijn Le fournisseur JPA s'assurera qu'à tout moment il n'y a qu'une seule instance d'une entité donnée dans le contexte, donc même vos ensembles sont sûrs sans implémenter equals / hascode, à condition que vous n'utilisiez que des entités gérées. La mise en œuvre de ces méthodes pour les entités n'est pas exempte de difficultés, par exemple, jetez un œil à cet article d'Hibernate sur le sujet. Mon point étant, si vous travaillez uniquement avec des entités gérées, vous êtes mieux sans elles, sinon fournissez une mise en œuvre très soignée.
Edwin Dalorzo
2
@TheStijn C'est le bon scénario mixte. Cela justifie la nécessité d'implémenter eq / hC comme vous l'aviez initialement suggéré, car une fois que les entités ont abandonné la sécurité de la couche de persistance, vous ne pouvez plus faire confiance aux règles appliquées par la norme JPA. Dans notre cas, le modèle DTO a été appliqué architecturalement depuis le début. De par sa conception, notre API de persistance n'offre pas de moyen public d'interagir avec les objets métier, uniquement une API pour interagir avec notre couche de persistance à l'aide de DTO.
Edwin Dalorzo
13

Mon ajout de 2 cents aux réponses ici est:

  1. En ce qui concerne l'accès aux champs ou aux propriétés (loin des considérations de performances), les deux sont légitimement accessibles au moyen de getters et de setters, donc ma logique de modèle peut les définir / les obtenir de la même manière. La différence vient à jouer lorsque le fournisseur d'exécution de persistance (Hibernate, EclipseLink ou autre) doit persister / définir un enregistrement dans le tableau A qui a une clé étrangère faisant référence à une colonne du tableau B. Dans le cas d'un type d'accès à la propriété, la persistance Le système d'exécution utilise ma méthode de définition codée pour affecter une nouvelle valeur à la cellule de la colonne du tableau B. Dans le cas d'un type d'accès au champ, le système d'exécution de persistance définit directement la cellule dans la colonne du tableau B. Cette différence n'a pas d'importance dans le contexte d'une relation unidirectionnelle, Pourtant, il est impératif d'utiliser ma propre méthode de définition codée (type d'accès à la propriété) pour une relation bidirectionnelle à condition que la méthode de définition soit bien conçue pour tenir compte de la cohérence. La cohérence est un problème critique pour les relations bidirectionnelles.lien pour un exemple simple pour un setter 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 entraînant une exception de débordement de pile. Une fois que vous essayez une relation bidirectionnelle (disons OneToOne) et que vous générez automatiquement Equals () ou hashCode () ou même toString (), vous serez pris dans cette exception de stackoverflow.

Sym-Sym
la source
9

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 les implémentations Equals / 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 plus un cas si ces entités JPA sont considérées comme des objets de valeur pilotés par domaine, au lieu d'entités pilotées par domaine (pour lesquelles ces exemples de code sont destinés).

ahaaman
la source
4
Bien que ce soit une bonne approche d'utiliser une classe d'entité parent pour retirer le code de plaque de chaudière, ce n'est pas une bonne idée d'utiliser l'ID défini par la base de données dans votre méthode equals. Dans votre cas, comparer 2 nouvelles entités lancerait même un NPE. Même si vous le mettez à zéro, 2 nouvelles entités seront toujours égales, jusqu'à ce qu'elles persistent. L'équation / hC doit être immuable.
Stijn Geukens
2
Equals () ne lancera pas NPE car il est vérifié si DB id est nul ou non & dans le cas où id id est nul, l'égalité serait fausse.
ahaaman
3
En effet, je ne vois pas comment j'ai manqué que le code soit null-safe. Mais l'OMI utilisant l'identifiant est toujours une mauvaise pratique. Arguments: onjava.com/pub/a/onjava/2006/09/13/…
Stijn Geukens
Dans le livre «Implémentation DDD» de Vaughn Vernon, il est avancé que vous pouvez utiliser id pour égal si vous utilisez la «génération PK précoce» (Générez d'abord un id et transmettez-le au constructeur de l'entité au lieu de laisser la base de données générer l'id lorsque vous persistez l'entité.)
Wim Deblauwe
ou si vous ne prévoyez pas de comparer des entités non persistantes? Pourquoi devriez-vous ...
Enerccio