web-dev-qa-db-fra.com

Remplacer un ORM complet (JPA/Hibernate) par une solution plus légère: modèles recommandés pour le chargement/la sauvegarde?

Je développe une nouvelle application Web Java et j'explore de nouvelles façons (nouvelles pour moi!) De conserver les données. J'ai surtout de l'expérience avec JPA & Hibernate mais, à l'exception de cas simples, je pense que ce type de gestion à part entière peut devenir assez complexe. De plus, je n'aime pas trop travailler avec eux. Je cherche une nouvelle solution, probablement plus proche de SQL.

Les solutions que je recherche actuellement:

  • MyBatis
  • JOOQ
  • Plain SQL/JDBC, éventuellement avec DbUtils ou d'autres bibliothèques utilitaires de base.

Mais je m'inquiète de deux cas d'utilisation de ces solutions, comparé à Hibernate. J'aimerais savoir quels sont les modèles recommandés pour ces cas d'utilisation.


Cas d'utilisation 1 - Récupération d'une entité et accès à certaines de ses entités enfants et petits-enfants associés.

  • Disons que j'ai une entité Person .
    • Cette Person a une entité Address associée .
      • Cette Address a une entité City associée .
        • Cette entité City a une propriété name.

Le chemin complet pour accéder au nom de la ville, à partir de l'entité personne, serait: 

person.address.city.name

Maintenant, supposons que je charge l'entité Person à partir d'une PersonService, avec cette méthode:

public Person findPersonById(long id)
{
    // ...
}

En utilisant Hibernate, les entités associées à Person pourraient être chargées paresseusement, à la demande, de sorte qu'il serait possible d'accéder à person.address.city.name et de vous assurer que j'ai accès à cette propriété (tant que toutes les entités de cette chaîne ne sont pas nullables).

Mais en utilisant l’une des 3 solutions sur lesquelles j’enquête, c’est plus compliqué. Avec ces solutions, quels sont les modèles recommandés pour prendre en charge ce cas d'utilisation? Au début, je vois 3 modèles possibles:

  1. Toutes les entités enfants et petits-enfants associés nécessaires pourraient être chargées avec impatience par la requête SQL utilisée. 

    Mais le problème que je vois avec cette solution est qu’il peut y avoir un autre code qui doit accéder aux autres} chemins d’entités/propriétés de l’entité Person. Par exemple, certains codes devront peut-être accéder à person.job.salary.currency. Si je veux réutiliser la méthode findPersonById() que j'ai déjà, la requête SQL devra alors charger plus d'informations! Non seulement l'entité address->city associée, mais également l'entité job->salary associée. 

    Maintenant, que se passe-t-il s'il y a 10 autres lieux qui ont besoin d'accéder à d'autres informations à partir de l'entité personne? Devrais-je toujours charger avec impatience tous les informations potentiellement nécessaires? Ou peut-être avez-vous 12 méthodes de service différentes pour charger une entité de personne? :

    findPersonById_simple(long id)
    
    findPersonById_withAdressCity(long id)
    
    findPersonById_withJob(long id)
    
    findPersonById_withAdressCityAndJob(long id)
    
    ...
    

    Mais ensuite, chaque fois que j'utiliserais une entité Person, il faudrait que je sache ce qui a été chargé et ce qui ne l'a pas été… Cela pourrait être assez lourd, non?

  2. Dans la méthode getAddress() getter de l'entité Person, pourrait-on vérifier si l'adresse a déjà été chargée et, si ce n'est pas le cas, la charger paresseusement? Est-ce un motif fréquemment utilisé dans des applications réelles?

  3. Existe-t-il d'autres modèles pouvant être utilisés pour m'assurer que je peux accéder aux entités/propriétés dont j'ai besoin à partir d'un modèle chargé?


Cas d'utilisation 2 - Enregistrer une entité et s'assurer que ses entités associées et modifiées sont également enregistrées.

Je veux pouvoir sauvegarder une entité Person en utilisant la méthode de cette PersonService:

public void savePerson(Person person)
{
    // ...
}

Si j'ai une entité Person et que je change person.address.city.name en quelque chose d'autre, comment puis-je m'assurer que les modifications de l'entité City seront conservées lorsque je sauvegarde la Person? Avec Hibernate, il est facile de mettre en cascade l'opération de sauvegarde sur les entités associées. Qu'en est-il des solutions sur lesquelles je cherche?

  1. Devrais-je utiliser une sorte de drapeau sale pour savoir quelles entités associées doivent également être enregistrées lorsque je sauve la personne?

  2. Existe-t-il d'autres modèles connus utiles pour traiter ce cas d'utilisation?


Mise à jour: Il y a une discussion à propos de cette question sur le forum JOOQ.

35
electrotype

Ce type de problème est typique lorsque vous n'utilisez pas de véritable ORM, et il n'y a pas de solution miracle. Une approche de conception simple qui a fonctionné pour moi pour une application Web (pas très grande) avec iBatis (myBatis) consiste à utiliser deux couches pour la persistance:

  • Une couche de bas niveau stupide: chaque table a sa classe Java (POJO ou DTO), avec champs qui mappent directement sur les colonnes de la table. Supposons que nous ayons une table PERSON avec un champ ADDRESS_ID qui pointe vers une table ADRESS; Nous aurions alors une classe PersonDb, avec juste un champ addressId (entier); nous n'avons pas de méthode personDb.getAdress(), seulement la personDb.getAdressId() simple. Ces classes Java sont donc assez stupides (elles ne connaissent ni la persistance ni les classes associées). Une classe PersonDao correspondante sait comment charger/conserver cet objet. Cette couche est facile à créer et à gérer avec des outils tels que iBatis + iBator (ou MyBatis + MYBatisGenerator ).

  • Une couche de niveau supérieur contenant les objets rich domain: chacun de ces éléments est généralement un graph des POJO ci-dessus. Ces classes ont également l’intelligence nécessaire pour charger/enregistrer le graphique (peut-être paresseusement, avec peut-être des indicateurs sales) en appelant les DAO respectives. Cependant, l’important est que ces objets de domaine riche ne mappent pas un à un vers les objets POJO (ou les tables de base de données), mais plutôt avec des cas d’utilisation domain. La "taille" de chaque graphique est déterminée (elle ne croît pas indéfiniment) et est utilisée de l'extérieur comme une classe particulière. Ainsi, ce n'est pas que vous ayez une classe Person riche (avec un graphe indéterminé d'objets associés) qui est utilisée dans plusieurs cas d'utilisation ou méthodes de service; à la place, vous avez plusieurs classes riches, PersonWithAddreses, PersonWithAllData... chacune d'elles encapsule un graphe bien limité, avec sa propre logique de persistance. Cela peut sembler inefficace ou maladroit, et dans certains contextes, mais il arrive souvent que les cas d'utilisation pour lesquels vous devez enregistrer un graphique complet d'objets sont réellement limités.

  • En outre, pour des éléments tels que des rapports tabulaires ((des SÉLECTIONS spécifiques renvoyant un ensemble de colonnes à afficher)), vous n'utiliseriez pas ce qui précède, mais des POJO simples et muets (peut-être même des cartes).

Voir ma réponse liée ici

28
leonbloy

La réponse à vos nombreuses questions est simple. Vous avez trois choix.

  1. Utilisez l’un des trois outils centrés sur SQL que vous avez mentionnés ( MyBatis , jOOQ , DbUtils ). Cela signifie que vous devriez cesser de penser en termes de modèle de domaine OO et de mappage objet-relationnel (c'est-à-dire entités et chargement différé). SQL concerne les données relationnelles et RBDMS calcule assez bien les plans d'exécution pour "extraire avec empressement" le résultat de plusieurs jointures. Généralement, la mise en cache prématurée n’est même pas vraiment nécessaire, et si vous avez besoin de mettre en cache un élément de données occasionnel, vous pouvez toujours utiliser quelque chose comme EhCache .

  2. N'utilisez aucun de ces outils centrés sur SQL et ne vous contentez pas d'Hibernate/JPA. Parce que même si vous dites que vous n'aimez pas Hibernate, vous "pensez à Hibernate". Hibernate sait très bien persister les graphes d’objets dans la base de données. Aucun de ces outils ne peut être forcé à travailler comme Hibernate, car leur mission est autre chose. Leur mission est d'opérer sur SQL.

  3. Allez d'une manière totalement différente et choisissez de ne pas utiliser un modèle de données relationnel. D'autres modèles de données (graphiques, par exemple) peuvent mieux vous convenir. Je propose cela comme une troisième option, car vous n’auriez peut-être pas le choix, et j’ai peu d’expérience personnelle avec les modèles alternatifs.

Remarque, votre question ne portait pas spécifiquement sur jOOQ. Néanmoins, avec jOOQ, vous pouvez externaliser le mappage des résultats de requête simples (produits à partir de sources de table jointes) en objets graphiques à l'aide d'outils externes tels que ModelMapper . Il existe un fil continu intéressant sur une telle intégration sur le groupe d'utilisateurs ModelMapper .

(disclaimer: je travaille pour l'entreprise derrière jOOQ)

28
Lukas Eder

Approches de persistance

Le spectre des solutions simples/basiques à sophistiquées/riches est:

  • SQL/JDBC - SQL codé en dur dans les objets
  • Structure basée sur SQL (par exemple, jOOQ, MyBatis) - Modèle d'enregistrement actif (un objet général séparé représente les données de ligne et gère le code SQL)
  • ORM-Framework (par exemple, Hibernate, EclipseLink, DataNucleus) - Modèle de mappeur de données (objet par entité) plus un modèle d'unité de travail (Gestionnaire de contenu/contexte de persistance)

Vous souhaitez implémenter l'un des deux premiers niveaux. Cela signifie que le modèle d'objet se concentre davantage sur le langage SQL. Mais votre question demande des cas d’utilisation impliquant le modèle d’objet en cours de mappage vers SQL (comportement ORM). Vous souhaitez ajouter des fonctionnalités du troisième niveau aux fonctionnalités de l’un des deux premiers niveaux. 

Nous pourrions essayer d'implémenter ce comportement dans un enregistrement actif. Mais pour cela, il faudrait associer des métadonnées riches à chaque instance Active Record - l'entité réelle impliquée, ses relations avec d'autres entités, les paramètres de chargement différé, les paramètres de mise à jour en cascade. Cela en ferait un objet entité mappé caché. De plus, jOOQ et MyBatis ne le font pas pour les cas d'utilisation 1 et 2.

Comment répondre à vos demandes? 

Implémentez un comportement ORM étroit directement dans vos objets, en tant que petit calque personnalisé au-dessus de votre infrastructure ou en SQL/JDBC brut. 

Cas d'utilisation 1: Stockez des métadonnées pour chaque relation d'objet entité: (i) si la relation doit être chargée paresseuse (niveau classe) et (ii) si un chargement paresseux s'est produit (niveau objet). Ensuite, dans la méthode getter, utilisez ces indicateurs pour déterminer s’il faut effectuer le chargement paresseux et le faire réellement. 

Cas d'utilisation 2: similaire au cas d'utilisation 1 - faites-le vous-même. Stocker un drapeau sale dans chaque entité. Pour chaque relation d’objet entité, stockez un indicateur indiquant si la sauvegarde doit être mise en cascade. Puis, lorsqu'une entité est enregistrée, visitez de manière récursive chaque relation de "sauvegarde en cascade". Écrivez toutes les entités sales découvertes. 

Patterns

Avantages

  • Les appels au framework SQL sont simples.

Les inconvénients

  • Vos objets deviennent plus compliqués. Examinez le code des cas d'utilisation 1 et 2 dans un produit open source. Ce n'est pas anodin
  • Manque de support pour le modèle d'objet. Si vous utilisez un modèle objet en Java pour votre domaine, la prise en charge des opérations de données sera moindre. 
  • Risque de fluage de la portée et d’anti-motifs: la fonctionnalité manquante ci-dessus est la partie visible de l’iceberg. Peut-être finira-t-il par réinventer le problème des roues et de l'infrastructure dans la logique d'entreprise.
  • Education et maintenance sur solution non standard. JPA, JDBC et SQL sont des standards. D'autres frameworks ou solutions personnalisées ne le sont pas.

Digne d'intérêt???

Cette solution fonctionne bien si vous avez des exigences relativement simples en matière de traitement des données et un modèle de données avec un nombre moins élevé d’entités: 

  • Si oui, génial! Faire ci-dessus.
  • Sinon, cette solution est mal adaptée et représente de fausses économies d’effort - c’est-à-dire que cela prendra plus de temps et sera plus compliqué que d’utiliser un ORM. Dans ce cas, jetez un autre coup d'œil à JPA - il est peut-être plus simple que vous ne le pensez et il prend en charge ORM pour CRUD plus SQL brut pour les requêtes complexes :-).
11
Glen Best

Les dix dernières années, j’utilisais JDBC, les beans entity EJB, Hibernate, GORM et enfin JPA (dans cet ordre). Pour mon projet actuel, je suis revenu à l’utilisation de JDBC, car l’accent est mis sur les performances. Donc je voulais

  • Contrôle total sur la génération des instructions SQL: Pouvoir transmettre une instruction aux optimistes de la base de données et remettre la version optimisée dans le programme.
  • Contrôle total sur le nombre des instructions SQL envoyées à la base de données
  • Procédures stockées (déclencheurs), fonctions stockées (pour les calculs complexes dans les requêtes SQL)
  • Pour pouvoir utiliser toutes les fonctionnalités SQL disponibles sans restrictions (requêtes récursives avec des CTE, fonctions d'agrégat de fenêtre, ...)

Le modèle de données est défini dans un dictionnaire de données; En utilisant une approche basée sur un modèle, un générateur crée des classes auxiliaires, des scripts DDL, etc. La plupart des opérations sur la base de données sont en lecture seule; seuls quelques cas d'utilisation écrivent.

Question 1: Aller chercher les enfants

Le système est construit sur des cas d'utilisation et nous avons une instruction SQL dédiée pour obtenir toutes les données pour un cas d'utilisation/demande donné. Certaines des instructions SQL dépassent 20 Ko, elles sont jointes, calculées à l'aide de fonctions stockées écrites en Java/Scala, triées, paginées, etc. de manière à ce que le résultat soit directement mappé dans un objet de transfert de données, qui est ensuite introduit dans la vue (pas de traitement ultérieur dans la couche application). En conséquence, l'objet de transfert de données est également spécifique à un cas d'utilisation. Il ne contient que les données pour le cas d'utilisation donné (rien de plus, rien de moins).

Comme le jeu de résultats est déjà "entièrement joint", il n’est pas nécessaire de procéder à une extraction paresseuse/désireuse, etc. L’objet de transfert de données est terminé. Un cache n'est pas nécessaire (le cache de la base de données est correct); l'exception: si l'ensemble de résultats est volumineux (environ 50 000 lignes), l'objet de transfert de données est utilisé comme valeur de cache.

Question 2: Sauvegarde

Une fois que le contrôleur a fusionné les modifications depuis l'interface graphique, il existe à nouveau un objet spécifique qui contient les données: en gros, les lignes avec un état (nouveau, supprimé, modifié, ...) dans une hiérarchie. Il s'agit d'une itération manuelle pour enregistrer les données dans la hiérarchie: les données nouvelles ou modifiées sont conservées à l'aide de certaines classes auxiliaires avec des commandes SQL SQL insert ou update. Quant aux éléments supprimés, ils sont optimisés pour les suppressions en cascade (PostgreSql). Si plusieurs lignes doivent être supprimées, elles sont également optimisées dans une seule instruction delete ... where id in ....

Encore une fois, il s’agit d’un cas d’utilisation spécifique, il n’ya donc pas d’approche générale. Il faut plus de lignes de code, mais ce sont les lignes qui contiennent les optimisations.

Les expériences jusqu'à présent

  • Il ne faut pas sous-estimer les efforts pour apprendre Hibernate ou JPA. Il convient de prendre en compte le temps nécessaire à la configuration des caches, à l'invalidation du cache dans un cluster, à l'extraction rapide/paresseuse et au réglage. La migration vers une autre version majeure d'Hibernate n'est pas simplement une recompilation.
  • Il ne faut pas surestimer les efforts pour créer une application sans ORM.
  • Il est plus simple d'utiliser SQL directement - être close en SQL (comme HQL, JPQL) est not identique, surtout si vous parlez à votre tuner de performances de base de données.
  • Les serveurs SQL sont incroyablement rapides lors de l'exécution de requêtes longues et complexes, surtout si elles sont combinées avec des fonctions stockées écrites dans Scala
  • Même avec les instructions SQL spécifiques aux cas d'utilisation, la taille du code est plus petite: les ensembles de résultats "entièrement joints" sauvegardent de nombreuses lignes dans la couche d'application.

Informations connexes:

Mise à jour: interfaces

S'il existe une entité Person dans le modèle de données logique, une classe permet de conserver une entité Person (pour les opérations CRUD), tout comme une entité JPA.

Mais le clou est qu’il n’existe aucune entité Person du point de vue cas d’utilisation/requête/logique métier: chaque méthode de service définit sa notion own de Person et ne contient que exactement les valeurs requises par ce cas d'utilisation. Ni plus ni moins. Nous utilisons Scala, de sorte que la définition et l’utilisation de nombreuses petites classes sont très efficaces (aucun code de plaque de chaudière n’est nécessaire).

Exemple:

class GlobalPersonUtils {
  public X doSomeProcessingOnAPerson(
    Person person, PersonAddress personAddress, PersonJob personJob, 
    Set<Person> personFriends, ...)
}

est remplacé par

class Person {
  List addresses = ...
  public X doSomeProcessingOnAPerson(...)
}

class Dto {
  List persons = ...
  public X doSomeProcessingOnAllPersons()
  public List getPersons()
}

utilisation de Persons, Adresses, etc. spécifiques à un cas d'utilisation: Dans ce cas, la Person regroupe déjà toutes les données pertinentes. Nécessite plus de classes, mais il n'est pas nécessaire de faire circuler des entités JPA.

Notez que ce traitement est en lecture seule, les résultats sont utilisés par la vue. Exemple: Obtenir des instances City distinctes de la liste d'une personne.

Si les données sont modifiées, il s'agit d'un autre cas d'utilisation: si la ville d'une personne est modifiée, elle est traitée par une méthode de service différente et la personne est extraite à nouveau de la base de données.

7
Beryllium

Avertissement: Ceci est une autre fiche éhontée d'un auteur de projet.

Découvrez JSimpleDB et voyez si cela répond à vos critères de simplicité vs puissance. Il s’agit d’un nouveau projet né de plus de 10 ans de frustration, qui tentait de gérer la persistance de Java via SQL et ORM.

Il fonctionne par-dessus toute base de données pouvant fonctionner comme magasin de clés/valeurs, par exemple, FoundationDB (ou n’importe quelle base de données SQL, bien que tout soit bloqué dans une seule table clé/valeur).

2
Archie

Il semble que le problème central de la question concerne le modèle relationnel lui-même. À partir de ce qui a été décrit, une base de données graphique mappera le domaine du problème de manière très nette. Les magasins de documents sont une autre façon d’aborder le problème car, même si l’impédance est toujours là, les documents sont en général plus simples à raisonner que les ensembles. Bien sûr, toute approche aura ses propres bizarreries.

1
CurtainDog

Puisque vous voulez une bibliothèque simple et légère et que vous utilisiez SQL, je peux vous suggérer de regarder fjorm . Il vous permet d’utiliser des opérations POJO et CRUD sans trop d’efforts.

Disclaimer: Je suis un auteur du projet.

0
Mladen Adamovic