Créez l’entité JPA parfaite

Je travaille avec JPA (implémentation Hibernate) depuis un certain temps et chaque fois que j’ai besoin de créer des entités, je me trouve confronté à des problèmes tels que AccessType, propriétés immuables, égal à / hashCode, ….
J’ai donc décidé d’essayer de trouver les meilleures pratiques générales pour chaque numéro et de les noter pour un usage personnel.
Cela ne me dérangerait cependant pas que quiconque puisse le commenter ou me dire où je me trompe.

Classe d’entité

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.

  • à côté de ce constructeur: avoir un constructeur constructeur par défaut privé

    Raison: Le constructeur par défaut est requirejs pour qu’Hibernate initialise l’entité. private est autorisé mais la visibilité privée (ou publique) du package est requirejse pour la génération de proxy à l’exécution et la récupération efficace des données sans instrumentation par bytecode.

Champs / Propriétés

  • Utiliser l’access aux champs en général et l’access aux propriétés lorsque cela est nécessaire

    Raison: il s’agit probablement de la question la plus discutable car il n’existe pas d’arguments clairs et convaincants pour l’un ou l’autre (access à la propriété et access aux champs); Cependant, l’access au champ semble être un favori général en raison d’un code plus clair, d’une meilleure encapsulation et de la nécessité de créer des parameters pour les champs immuables.

  • Omettre les parameters pour les champs immuables (non requirejs pour le champ de type d’access)

  • les propriétés peuvent être privées
    Raison: Une fois, j’ai entendu dire que la protection est meilleure pour les performances (Hibernate) mais tout ce que je peux trouver sur le Web est: Hibernate peut accéder directement aux méthodes d’access public, privé et protégé, ainsi qu’aux champs publics, privés et protégés. Le choix vous appartient et vous pouvez le faire correspondre à la conception de votre application.

Égal à / hashCode

  • N’utilisez jamais un identifiant généré si cet identifiant est défini uniquement 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 UUID non-transitoire créé lors de l’initialisation de l’entité. Voir ce grand article pour plus d’informations.
  • ne jamais faire référence à des entités liées (ManyToOne); Si cette entité (comme une entité parente) doit faire partie de la clé métier, comparez uniquement les identifiants. L’appel de getId () sur un proxy ne déclenche pas le chargement de l’entité, tant que vous utilisez le type d’access à 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 Ssortingng 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, Ssortingng 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 Ssortingng 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 à append à cette liste sont les bienvenues …

METTRE À JOUR

Depuis la lecture de cet article, j’ai adapté ma manière d’implémenter eq / hC:

  • si une clé métier simple immuable est disponible: utilisez-la
  • dans tous les autres cas: utilisez un uuid

La spécification JPA 2.0 stipule 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 premier niveau. 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’object 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é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és primaires et les clés de mappage, à ma connaissance.

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

Classe d’entité: implémenter Serializable?

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

Constructeurs: créez un constructeur avec tous les champs obligatoires de l’entité?

Les constructeurs pour la logique applicative ne doivent avoir 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 rest doit être défini en appelant les méthodes de réglage – c’est ce qu’elles sont pour.

Évitez de mettre trop de champs dans les constructeurs. Les constructeurs doivent être commodes et apporter une hygiène de base à l’object. Le nom, le type et / ou les parents sont généralement utiles.

OTOH si les règles d’application (d’aujourd’hui) imposent au Client d’avoir une Adresse, laissez cela à un installateur. C’est un exemple de “règle faible”. Peut-être que la semaine prochaine, vous voulez créer un object Client avant de passer à l’écran Enter Details? Ne vous trompez pas, laissez la possibilité de données inconnues, incomplètes ou «partiellement saisies».

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

Oui, mais utilisez ‘protected’ plutôt que le package private. Le sous-classement est une véritable douleur lorsque les composants internes nécessaires ne sont pas visibles.

Champs / Propriétés

Utilisez l’access au champ ‘property’ pour Hibernate et depuis l’extérieur de l’instance. Dans l’instance, utilisez directement les champs. Raison: permet à la reflection standard, la méthode la plus simple et la plus simple pour Hibernate, de fonctionner.

En ce qui concerne les champs “immuables” à l’application, Hibernate doit toujours pouvoir les charger. Vous pouvez essayer de rendre ces méthodes «privées» et / ou y placer une annotation pour empêcher le code d’application de créer des access indésirables.

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

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

Improbable.

Equal / HashCode?

Ceci est pertinent pour travailler avec des entités, avant qu’elles aient été enregistrées – ce qui est un problème épineux. Hashing / comparaison sur des valeurs immuables? Dans la plupart des applications professionnelles, il n’y en a pas.

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

Les quelques choses qui sont normalement conservées 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 de manière unique l’entité!

Donc, long et court, les données “immuables” revendiquées ne sont pas vraiment. Les champs Clé primaire / ID sont générés dans le but précis de fournir une stabilité et une immuabilité garanties.

Vous devez planifier et prendre en compte vos besoins en phases de travail de comparaison et de hachage et de traitement des demandes lorsque A) utiliser des “données modifiées / liées” de l’interface utilisateur si vous comparez / hachez des “champs rarement modifiés” ou B) données non enregistrées “, si vous comparez / hache sur l’ID.

Equals / HashCode – si une clé métier 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 si nécessaire. Sachez que les UUID ne sont pas libres, en termes de performances, et que le clustering complique les choses.

Equals / HashCode – ne jamais se référer à des entités liées

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

Cela semble être un bon conseil.

J’espère que cela t’aides!

Mes 2 cents supplémentaires aux réponses ici sont:

  1. En ce qui concerne l’access aux champs ou aux propriétés (loin des considérations de performances), les deux sont accessibles légitimement au moyen de getters et de seters, ainsi, la logique de mon modèle peut les définir / obtenir de la même manière. La différence vient de jouer lorsque le fournisseur d’exécution de persistance (Hibernate, EclipseLink ou autre) doit conserver / définir un enregistrement dans la table A qui a une clé étrangère faisant référence à une colonne du tableau B. Dans le cas d’un type d’access de propriété, la persistance Le système d’exécution utilise ma méthode de réglage codé pour atsortingbuer à la cellule de la colonne du tableau B une nouvelle valeur. Dans le cas d’un type d’access au champ, le système d’exécution de persistance définit directement la cellule de la colonne du tableau B. Cette différence n’a pas d’importance dans le contexte d’une relation unidirectionnelle, mais il est impératif d’utiliser ma propre méthode de réglage codé (type d’access à la propriété) pour une relation bidirectionnelle, à condition que la méthode de réglage soit bien conçue pour assurer la cohérence. . La cohérence est un problème critique pour les relations bidirectionnelles. Reportez-vous à ce lien pour obtenir un exemple simple d’un système de réglage 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 stackoverflow. Une fois que vous avez essayé une relation bidirectionnelle (par exemple, OneToOne) et généré automatiquement Equals () ou hashCode () ou même toSsortingng (), vous serez intercepté dans cette exception stackoverflow.

Interface d’entité

 public interface Entity 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 Equal / Hashcode:

 public abstract class AbstractEntity implements Entity { @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 Ssortingng toSsortingng() { return getClass().getSimpleName() + ": " + identity(); // OR // return ReflectionToSsortingngBuilder.reflectionToSsortingng(this, ToSsortingngStyle.MULTI_LINE_STYLE); } } 

Room Entity impl:

 @Entity @Table(name = "ROOM") public class Room extends AbstractEntity { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "room_id") private Integer id; @Column(name = "number") private Ssortingng 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, Ssortingng 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 Ssortingng 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é JPA. Cela pourrait être plus un cas si ces entités JPA sont considérées comme des ValueObjects pilotés par le domaine, plutôt que des entités pilotées par le domaine (auxquelles ces exemples de code sont destinés).