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:
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.
Person
.Person
a une entité Address
associée .Address
a une entité City
associée .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:
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?
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?
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é?
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?
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?
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.
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
La réponse à vos nombreuses questions est simple. Vous avez trois choix.
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 .
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.
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)
Approches de persistance
Le spectre des solutions simples/basiques à sophistiquées/riches est:
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 inconvénients
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:
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
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
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.
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).
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.
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.