Persistance des données avec des objets de données Java, partie 1

"Tout doit être rendu aussi simple que possible, mais pas plus simple."

Albert Einstein

La nécessité de conserver les données créées lors de l'exécution est aussi ancienne que l'informatique. Et le besoin de stocker des données orientées objet est apparu lorsque la programmation orientée objet est devenue omniprésente. Actuellement, la plupart des applications modernes et non triviales utilisent un paradigme orienté objet pour modéliser les domaines d'application. En revanche, le marché des bases de données est plus divisé. La plupart des systèmes de base de données utilisent le modèle relationnel, mais les magasins de données basés sur des objets s'avèrent indispensables dans de nombreuses applications. De plus, nous avons également des systèmes hérités avec lesquels nous devons souvent nous connecter.

Cet article identifie les problèmes associés à la persistance des données dans les environnements intergiciels transactionnels, tels que J2EE (Java 2 Platform, Enterprise Edition), et montre comment Java Data Objects (JDO) résout certains de ces problèmes. Cet article fournit une vue d'ensemble, pas un didacticiel détaillé, et est écrit du point de vue d'un développeur d'applications, et non d'un concepteur d'implémentation JDO.

Lisez toute la série sur les objets de données Java:

  • Partie 1. Saisir les qualités d'une couche de persistance idéale
  • Partie 2. Sun JDO contre Castor JDO

Les développeurs, concepteurs et architectes J2EE Java qui travaillent sur des systèmes qui doivent stocker des données dans des bases de données relationnelles ou d'objets, ou dans d'autres supports de stockage, doivent lire cet article. Je suppose que vous avez une connaissance de base de Java et une certaine familiarité avec les problèmes relationnels objet et la terminologie.

Persistance transparente: pourquoi s'embêter?

Plus d'une décennie de tentatives continues pour relier l'exécution orientée objet et la persistance indiquent plusieurs observations importantes (classées par ordre d'importance):

  1. Abstraire tous les détails de persistance et disposer d'une API propre, simple et orientée objet pour effectuer le stockage des données est primordial. Nous ne voulons pas gérer les détails de persistance et la représentation des données internes dans les magasins de données, qu'ils soient relationnels, basés sur des objets ou autre. Pourquoi devrions-nous traiter les constructions de bas niveau du modèle de magasin de données, telles que les lignes et les colonnes, et les traduire constamment dans les deux sens? Au lieu de cela, nous devons nous concentrer sur cette application complexe que nous devions livrer hier.
  2. Nous voulons utiliser l'approche plug-and-play avec nos magasins de données: nous voulons utiliser différents fournisseurs / implémentations sans changer une ligne du code source de l'application - et peut-être sans modifier plus de quelques lignes dans le fichier de configuration approprié ( s). En d'autres termes, nous avons besoin d'une norme industrielle pour accéder aux données basées sur des objets Java, une norme qui joue un rôle similaire à celui que JDBC (Java Database Connectivity) joue en tant que norme industrielle pour accéder aux données SQL.
  3. Nous voulons utiliser l'approche plug-and-play avec différents paradigmes de base de données - c'est-à-dire que nous voulons passer d'une base de données relationnelle à une base de données orientée objet avec des modifications minimales du code de l'application. Bien que cela soit agréable à avoir, dans la pratique, cette capacité n'est souvent pas requise.

    Un commentaire ici: alors que les bases de données relationnelles bénéficient de loin de la plus grande présence sur le marché, il est logique de fournir une API de persistance unifiée et de permettre aux fournisseurs de stockage de données de rivaliser sur les forces de mise en œuvre, quel que soit le paradigme utilisé par ces fournisseurs. Cette approche pourrait éventuellement aider à uniformiser les règles du jeu entre les deux groupes dominants de fournisseurs de bases de données: le camp relationnel bien enraciné et le camp orienté objet en lutte pour la part de marché.

Les trois découvertes énumérées ci-dessus nous amènent à définir une couche de persistance, un cadre qui fournit une API Java de haut niveau pour les objets et les relations afin de survivre à la durée de vie de l'environnement d'exécution (JVM). Un tel cadre doit présenter les qualités suivantes:

  • Simplicité
  • Intrusion minimale
  • Transparence, ce qui signifie que le cadre cache la mise en œuvre du magasin de données
  • API cohérentes et concises pour le stockage / la récupération / la mise à jour d'objets
  • Prise en charge des transactions, ce qui signifie que le cadre définit la sémantique transactionnelle associée aux objets persistants
  • Prise en charge des environnements gérés (par exemple, basés sur un serveur d'applications) et non gérés (autonomes)
  • Prise en charge des extras nécessaires, tels que la mise en cache, les requêtes, la génération de clé primaire et les outils de mappage
  • Frais de licence raisonnables - pas une exigence technique, mais nous savons tous qu'une mauvaise économie peut condamner un excellent projet

Je détaille la plupart des qualités ci-dessus dans les sections suivantes.

Simplicité

La simplicité figure en bonne place sur ma liste de caractéristiques requises pour tout cadre logiciel ou bibliothèque (voir la citation d'ouverture de cet article). Développer des applications distribuées est déjà déjà assez difficile et de nombreux projets logiciels échouent en raison d'une mauvaise gestion de la complexité (et, par extension, des risques). Simple n'est pas synonyme de simpliste; le logiciel doit avoir toutes les fonctionnalités nécessaires pour permettre à un développeur de faire son travail.

Intrusion minimale

Chaque système de stockage persistant introduit une certaine quantité d'intrusion dans le code de l'application. La couche de persistance idéale doit minimiser les intrusions pour obtenir une meilleure modularité et, par conséquent, une fonctionnalité plug-and-play.

Aux fins de cet article, je définis l'intrusion comme:

  • La quantité de code spécifique à la persistance éclaboussé dans le code de l'application
  • La nécessité de modifier votre modèle d'objet d'application en ayant à implémenter une interface de persistance - telle que Persistableou similaire - ou en post-traitant le code généré

L'intrusion s'applique également aux systèmes de base de données orientés objet et, bien que généralement moins problématique par rapport aux magasins de données relationnelles, elle peut varier considérablement entre les fournisseurs d'ODBMS (système de gestion de base de données orienté objet).

Transparence

Le concept de transparence de la couche persistante est assez simple: l'application utilise la même API quel que soit le type de magasin de données (transparence de type stockage de données) ou le fournisseur de magasin de données (transparence du fournisseur de stockage de données). La transparence simplifie considérablement les applications et améliore leur maintenabilité en masquant au maximum les détails d'implémentation du magasin de données. En particulier, pour les magasins de données relationnelles les plus répandus, contrairement à JDBC, vous n'avez pas besoin de coder en dur les instructions SQL ou les noms de colonne, ni de vous souvenir de l'ordre des colonnes renvoyé par une requête. En fait, vous n'avez pas besoin de connaître SQL ou l'algèbre relationnelle, car ils sont trop spécifiques à l'implémentation. La transparence est peut-être le trait le plus important de la couche de persistance.

API cohérente et simple

L'API de la couche de persistance se résume à un ensemble d'opérations relativement restreint:

  • Opérations CRUD élémentaires (créer, lire, mettre à jour, supprimer) sur des objets de première classe
  • Gestion des transactions
  • Gestion des identités des objets d'application et de persistance
  • Gestion du cache (c.-à-d. Actualisation et suppression)
  • Création et exécution de requêtes

Un exemple d' PersistenceLayerAPI:

public void persist (Object obj); // Enregistrer obj dans le magasin de données. public Object load (Classe c, Object pK); // Lit obj avec une clé primaire donnée. mise à jour publique void (Object obj); // Mettre à jour l'objet modifié obj. public void delete (Object obj); // Supprime obj de la base de données. public Collection find (Query q); // Trouver des objets qui satisfont aux conditions de notre requête.

Prise en charge des transactions

Une bonne couche de persistance a besoin de plusieurs fonctions élémentaires pour démarrer, valider ou annuler une transaction. Voici un exemple:

// Transaction (tx) demarcation. public void startTx(); public void commitTx(); public void rollbackTx(); // Choose to make a persistent object transient after all. public void makeTransient(Object o) 

Note: Transaction demarcation APIs are primarily used in nonmanaged environments. In managed environments, the built-in transaction manager often assumes this functionality.

Managed environments support

Managed environments, such as J2EE application servers, have grown popular with developers. Who wants to write middle tiers from scratch these days when we have excellent application servers available? A decent persistence layer should be able to work within any major application server's EJB (Enterprise JavaBean) container and synchronize with its services, such as JNDI (Java Naming and Directory Interface) and transaction management.

Queries

The API should be able to issue arbitrary queries for data searches. It should include a flexible and powerful, but easy-to-use, language -- the API should use Java objects, not SQL tables or other data-store representations as formal query parameters.

Cache management

Cache management can do wonders for application performance. A sound persistence layer should provide full data caching as well as appropriate APIs to set the desired behavior, such as locking levels, eviction policies, lazy loading, and distributed caching support.

Primary key generation

Providing automatic identity generation for data is one of the most common persistence services. Every decent persistence layer should provide identity generation, with support for all major primary key-generation algorithms. Primary key generation is a well-researched issue and numerous primary key algorithms exist.

Mapping, for relational databases only

With relational databases, a data mapping issue arises: the need to translate objects into tables, and to translate relationships, such as dependencies and references, into additional columns or tables. This is a nontrivial problem in itself, especially with complex object models. The topic of object-relational model impedance mismatch reaches beyond this article's scope, but is well publicized. See Resources for more information.

The following list of extras related to mapping and/or relational data stores are not required in the persistence layer, but they make a developer's life much easier:

  • A GUI (graphical user interface) mapping tool
  • Code generators: Autogeneration of DDL (data description language) to create database tables, or autogeneration of Java code and mapping files from DDL
  • Primary key generators: Supporting multiple key-generation algorithms, such as UUID, HIGH-LOW, and SEQUENCE
  • Support for binary large objects (BLOBs) and character-based large objects (CLOBs)
  • Self-referential relations: An object of type Bar referencing another object of type Bar, for example
  • Raw SQL support: Pass-through SQL queries

Example

The following code snippet shows how to use the persistence layer API. Suppose we have the following domain model: A company has one or more locations, and each location has one or more users. The following could be an example application's code:

PersistenceManager pm =PMFactory.initialize(..); Company co = new Company("MyCompany"); Location l1 = new Location1 ("Boston"); Location l2 = new Location("New York"); // Create users. User u1 = new User("Mark"); User u2 = new User("Tom"); User u3 = new User("Mary"); // Add users. A user can only "belong" to one location. L1.addUser(u1); L1.addUser(u2); L2.addUser(u3); // Add locations to the company. co.addLocation(l1); co.addLocation(l2); // And finally, store the whole tree to the database. pm.persist(c); 

In another session, you can look up companies employing the user Tom:

PersistenceManager pm =PMFactory.initialize(...) Collection companiesEmployingToms = pm.find("company.location.user.name = 'Tom'"); 

Pour les magasins de données relationnelles, vous devez créer un fichier de mappage supplémentaire. Cela pourrait ressembler à ceci:

    Utilisateur Emplacements de l'entreprise             

La couche de persistance s'occupe du reste, qui comprend les éléments suivants:

  • Recherche de groupes d'objets dépendants
  • Gérer l'identité des objets d'application
  • Gestion des identités d'objet persistantes (clés primaires)
  • Persistance de chaque objet dans l'ordre approprié
  • Fournir une gestion du cache
  • Fournir le bon contexte transactionnel (nous ne voulons pas que seule une partie de l'arborescence d'objets persiste, n'est-ce pas?)
  • Fournir des modes de verrouillage sélectionnables par l'utilisateur