I. Migration de données▲
I-A. Pourquoi ne pas faire de dump ?▲
Le dump d'une base prend beaucoup de temps, d'autant plus si elle est de taille imposante ou qu'elle se trouve à distance. De plus, si on a besoin de n'injecter que certaines données dans une autre base, on n'a pas besoin de dumper toute la base. Aussi, pourquoi devrions-nous faire un dump d'une base si une unique donnée a changé, alors qu'il y a des solutions pour extraire et injecter des données sélectionnées ?
I-B. DbUnit▲
DbUnit est une extension de JUnit se présentant sous la forme d'un puissant outil d'extraction et d'injection de données. Elle permet, entre autres, d'exporter une base de données (ou une partie) sous forme de fichier XML et de l'importer dans une autre base.
La dépendance Maven est :
2.
3.
4.
5.
<dependency>
<groupId>
org.dbunit</groupId>
<artifactId>
dbunit</artifactId>
<version>
2.5.0</version>
</dependency>
I-B-1. Contexte▲
L'application d'une société délivrant des contrats à ses clients comprend différents composants. Chaque contrat a des transactions bancaires (paiement, remboursement, annulation de transaction, etc.). La base de données source (base de production par exemple) a rapidement grossi et le but du projet est de pouvoir sélectionner quelques clients et de les injecter dans une autre base. La structure de la base est la suivante :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
--
-- Structure de la table `t_client`
--
CREATE
TABLE
IF
NOT
EXISTS
`t_client`
(
`id`
int
(
10
)
NOT
NULL
,
`name`
varchar
(
50
)
NOT
NULL
,
`adresse`
varchar
(
255
)
NOT
NULL
,
PRIMARY
KEY
(
`id`
)
)
ENGINE
=
InnoDB DEFAULT
CHARSET
=
latin1;
--
-- Contenu de la table `t_client`
--
INSERT
INTO
`t_client`
(
`id`
, `name`
, `description`
)
VALUES
(
100
, 'Michel Besson'
, '89 quai Panhard et Levassor 75013'
)
,
(
200
, 'Olivier Lecarme'
, '89 quai Panhard et Levassor 75013'
)
,
(
300
, 'Carine Fédéle'
, '89 quai Panhard et Levassor 75013'
)
,
(
400
, 'Cécile Dupond'
, '89 quai Panhard et Levassor 75013'
)
,
(
500
, 'Bile Gate'
, '89 quai Panhard et Levassor 75013'
)
;
-- --------------------------------------------------------
-- --------------------------------------------------------
--
-- Structure de la table `t_transactions_bancaires`
--
CREATE
TABLE
IF
NOT
EXISTS
`t_transactions_bancaires`
(
`id`
int
(
10
)
NOT
NULL
,
`name`
varchar
(
50
)
NOT
NULL
,
`amount`
double
NOT
NULL
,
`contrat_fk`
int
(
10
)
NOT
NULL
,
PRIMARY
KEY
(
`id`
)
,
KEY
`contrat_fk_ind`
(
`contrat_fk`
)
)
ENGINE
=
InnoDB DEFAULT
CHARSET
=
latin1;
--
-- Contenu de la table `t_transactions_bancaires`
--
INSERT
INTO
`t_transactions_bancaires`
(
`id`
, `name`
, `amount`
, `contrat_fk`
)
VALUES
(
1
, 'Paiement de ''option Sport'
, 10
, 41
)
,
(
10
, 'Remboursement'
, 22
, 41
)
,
(
11
, 'Remboursement pour fidélité'
, 32
, 31
)
;
-- --------------------------------------------------------
--
-- Structure de la table `t_contrat`
--
CREATE
TABLE
IF
NOT
EXISTS
`t_contrat`
(
`id`
int
(
10
)
NOT
NULL
,
`name`
varchar
(
50
)
NOT
NULL
,
`description`
varchar
(
255
)
NOT
NULL
,
`client_fk`
int
(
10
)
NOT
NULL
,
PRIMARY
KEY
(
`id`
)
,
KEY
`client_fk_ind`
(
`client_fk`
)
)
ENGINE
=
InnoDB DEFAULT
CHARSET
=
latin1;
--
-- Contenu de la table `t_contrat`
--
INSERT
INTO
`t_contrat`
(
`id`
, `name`
, `description`
, `client_fk`
)
VALUES
(
31
, 'Abonnement Télé'
, 'Description du contrat'
, 400
)
,
(
32
, 'Abonnement Box TV'
, 'Chaines Sport, Documentaires etc..'
, 400
)
,
(
41
, 'Abonnement Video PLAY'
, 'description du contrat'
, 200
)
,
(
71
, 'Location'
, 'Location de divers biens'
, 300
)
;
--
-- Contraintes pour les tables exportées
--
--
-- Contraintes pour la table `t_transactions_bancaires`
--
ALTER
TABLE
`t_transactions_bancaires`
ADD
CONSTRAINT
`t_transactions_bancaires_ibfk_1`
FOREIGN
KEY
(
`contrat_fk`
)
REFERENCES
`t_contrat`
(
`id`
)
ON
DELETE
CASCADE
;
--u
-- Contraintes pour la table `t_contrat`
--
ALTER
TABLE
`t_contrat`
ADD
CONSTRAINT
`t_contrat_ibfk_1`
FOREIGN
KEY
(
`client_fk`
)
REFERENCES
`t_client`
(
`id`
)
ON
DELETE
CASCADE
;
Les bases cibles ne seront peuplées qu'avec les données dont elles ont besoin. Chaque action est gérée par une classe Java : DataExtractorMain.java et DataInjectorMain.java.
I-B-2. Extraction de données▲
Pour extraire une donnée, on a besoin d'une connexion (JDBC, par exemple) vers la base source. Pour l'injection, on a besoin d'une connexion vers la base cible. Dans tous les cas, on utilise Maven pour la gestion des dépendances.
L'extraction est gérée par ce service. Dans ce dernier, il y a trois exemples d'extraction de données, comme on peut les voir sur les commentaires dans la classe :
- export de données partielles de la base de données (fichier généré partial_data.xml) ;
- export de toute la base (fichier généré full_data.xml) ;
- export des tables dépendantes : export de la table T et de toutes les tables ayant une clé primaire qui est une étrangère sur la table T (fichier généré dependents.xml).
Le test génère les fichiers XML suivants : partial_data.xml, full_data.xml et dependents.xml. Ci-dessous le contenu de full_data.xml :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<t_client
id
=
"100"
name
=
"Michel Besson"
adresse
=
"89 quai Panhard et Levassor 75013"
/>
<t_client
id
=
"200"
name
=
"Olivier Lecarme"
adresse
=
"89 quai Panhard et Levassor 75013"
/>
<t_client
id
=
"300"
name
=
"Carine Dubois"
adresse
=
"89 quai Panhard et Levassor 75013"
/>
<t_client
id
=
"400"
name
=
"Lydie Dupond"
adresse
=
"89 quai Panhard et Levassor 75013"
/>
<t_client
id
=
"500"
name
=
"Bile Gate"
adresse
=
"89 quai Panhard et Levassor 75013"
/>
<t_contrat
id
=
"31"
name
=
"Abonnement TV VOD"
description
=
"Description du contrat"
client_fk
=
"400"
/>
<t_contrat
id
=
"32"
name
=
"Abonnement Box TV"
description
=
"Chaines Sport, Documentaires etc.."
client_fk
=
"400"
/>
<t_contrat
id
=
"41"
name
=
"Abonnement Video PLAY"
description
=
"description du contrat"
client_fk
=
"200"
/>
<t_contrat
id
=
"71"
name
=
"Location"
description
=
"Location de divers biens"
client_fk
=
"300"
/>
<t_transactions_bancaires
id
=
"1"
name
=
"Paiement option Sport"
amount
=
"10.0"
contrat_fk
=
"41"
/>
<t_transactions_bancaires
id
=
"10"
name
=
"Remboursement"
amount
=
"22.0"
contrat_fk
=
"41"
/>
<t_transactions_bancaires
id
=
"11"
name
=
"Remboursement pour promotion"
amount
=
"32.0"
contrat_fk
=
"31"
/>
</dataset></pre>
<pre>
I-B-2-a. Comment se fait l'exportation ? ▲
Exemple : export de données partielles de la base de données
Tout part de l'interface IDataSet qui représente une collection de tables. La classe QueryDataSet est une implémentation de cette interface et définit donc la méthode d'ajout de table (addTable()). On crée donc une instance de cette classe QueryDataSet à partir de la connexion, puis on appelle la méthode addTable() pour sélectionner et ajouter les données souhaitées. La dernière étape est la génération du fichier XML à partir de cette instance, et pour cela, on appelle la méthode write() de la classe FlatXmlDataSet qui génère le fichier XML.
Nous allons donc pouvoir injecter ces données dans une autre base.
I-B-3. Injection de données▲
Pour cela, nous avons besoin d'une connexion à la base source et au fichier XML qui sera mise en œuvre par la classe DataInjectorMain.java.
Ainsi, notre base cible B qui était vide se retrouve remplie avec les données extraites :
I-B-4. Import et export d'un client▲
Dans cet exemple pratique, nous voulons exporter un client avec toutes ses dépendances (détails, contrats et transactions bancaires) et l'injecter dans une base Z. DbUnit fournit également des services pour le faire.
Les traces de l'exécution :
Le fichier exporté, data_client_200.xml, se retrouve donc avec le contenu suivant :
2.
3.
4.
5.
6.
7.
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<t_client
id
=
"200"
name
=
"Olivier Lecarme"
adresse
=
"89 quai Panhard et Levassor 75013"
/>
<t_contrat
id
=
"41"
name
=
"Abonnement Video PLAY"
description
=
"description du contrat"
client_fk
=
"200"
/>
<t_transactions_bancaires
id
=
"1"
name
=
"Paiement option Sport"
amount
=
"10.0"
contrat_fk
=
"41"
/>
<t_transactions_bancaires
id
=
"10"
name
=
"Remboursement"
amount
=
"22.0"
contrat_fk
=
"41"
/>
</dataset>
Dans la base Z, on voit que le client est bien injecté avec ses contrats et ses transactions bancaires :
Comment se fait l'importation ?
C'est le même principe que dans la classe d'injection. Partant du fichier XML des données extraites, on crée une instance de IDatSet pour représenter cet ensemble de données. Ce dernier est décoré par une instance de l'interface ReplacementDataSet que l'on passera à la méthode execute() de la classe DatabaseOperation qui fera un nettoyage puis une insertion des données.
I-B-5. Remarques▲
I-B-5-a. Automatisation▲
De la même façon que nous pouvons extraire et injecter un client, nous pouvons aussi extraire toute une liste de clients via un service de recherche d'abonnés. Une fois ces abonnés trouvés, nous pourrons appeler la couche d'extraction pour extraire les abonnés. En plus de Maven pour la gestion des dépendances et Spring pour la gestion des cycles de vie des objets, nous pouvons aussi utiliser Jenkins pour l'automatisation des tâches (recherche, extraction et injection) et donc pouvoir approvisionner nos bases automatiquement.
I-B-5-b. Avantages et Inconvénients▲
Avec DbUnit, on arrive à extraire et injecter des données très volumineuses dans différentes bases, ce qui en fait un très bon outil pour la migration de données. Mais pour pouvoir l'utiliser, il faut que la base de données soit bien structurée (présence de clé primaire, clé étrangère, relation, etc.). Si ce n'est pas le cas, il faudra commencer par résoudre ce problème.
II. Tests▲
Le module Spring DbUnit offre une intégration entre le framework de test Spring et DbUnit. Il permet de charger et décharger des bases de données en utilisant de simples annotations pour insérer, supprimer ou modifier des données, permettant ainsi de tester les services de l'application.
Dans cette partie de l'article, nous allons voir comment faire pour tester un projet Hibernate JPA en utilisant une base de données Hypersonic en mémoire.
Dépendances Maven
Les principales dépendances utilisées par le projet sont les suivantes : DBunit, Spring, Slf4j. Vous retrouverez la liste exhaustive dans le pom.xml.
II-A. Le projet Hibernate JPA▲
II-A-1. Modèle de données : Entity▲
Le projet à tester étant du Hiberate JPA, nous pouvons définir l'entité Customer comme suit :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
package
fr.soat.icoundoul.dbunit.entity;
import
javax.persistence.Entity;
import
javax.persistence.Id;
import
javax.persistence.NamedQueries;
import
javax.persistence.NamedQuery;
@Entity
@NamedQueries
({
@NamedQuery
(
name =
"Customer.find"
, query =
"SELECT p from Customer p where p.firstName like :name "
+
"or p.lastName like :name"
) }
)
public
class
Customer {
@Id
private
int
id;
private
String title;
private
String firstName;
private
String lastName;
}
II-A-2. Fichier de persistance ▲
Le fichier XML de persistance est le suivant :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
<persistence
xmlns
=
"http://java.sun.com/xml/ns/persistence"
xmlns
:
xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
xsi
:
schemaLocation
=
"http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version
=
"2.0"
>
<persistence-unit
name
=
"pagingDatabase"
transaction-type
=
"RESOURCE_LOCAL"
>
<provider>
org.hibernate.ejb.HibernatePersistence</provider>
<class>
fr.soat.icoundoul.dbunit.entity.Customer</class>
<properties>
<property
name
=
"hibernate.dialect"
value
=
"org.hibernate.dialect.HSQLDialect"
/>
<property
name
=
"hibernate.hbm2ddl.auto"
value
=
"create-drop"
/>
<property
name
=
"hibernate.show_sql"
value
=
"true"
/>
<property
name
=
"hibernate.cache.provider_class"
value
=
"org.hibernate.cache.HashtableCacheProvider"
/>
</properties>
</persistence-unit>
</persistence>
II-A-3. Le service à tester▲
Le service à tester est celui de gestion des clients (customer), qui définit la méthode Search, permettant de trouver un client comme suit :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
package
fr.soat.icoundoul.dbunit.service;
import
java.util.List;
import
javax.persistence.EntityManager;
import
javax.persistence.PersistenceContext;
import
javax.persistence.Query;
import
org.springframework.stereotype.Service;
import
org.springframework.transaction.annotation.Transactional;
import
fr.soat.icoundoul.dbunit.entity.Customer;
@Service
@Transactional
public
class
CustomerService {
@PersistenceContext
private
EntityManager entityManager;
@SuppressWarnings
(
"unchecked"
)
public
List<
Customer>
find
(
String name) {
Query query =
entityManager.createNamedQuery
(
"Customer.find"
);
query.setParameter
(
"name"
, "%"
+
name +
"%"
);
return
query.getResultList
(
);
}
}
II-A-4. Tester le service▲
Afin de nous assurer que notre service fonctionne bien, nous devons le tester en créant un test JUnit comme suit :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
package
fr.soat.icoundoul.dbunit.test;
import
static
junit.framework.Assert.assertEquals;
import
java.util.List;
import
org.junit.Test;
import
org.junit.runner.RunWith;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.test.context.ContextConfiguration;
import
org.springframework.test.context.TestExecutionListeners;
import
org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import
org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import
com.github.springtestdbunit.DbUnitTestExecutionListener;
import
fr.soat.icoundoul.dbunit.entity.Customer;
import
fr.soat.icoundoul.dbunit.service.CustomerService;
@RunWith
(
SpringJUnit4ClassRunner.class
)
@ContextConfiguration
(
locations =
{
"/applicationContext.xml"
}
)
@TestExecutionListeners
({
DependencyInjectionTestExecutionListener.class
, DbUnitTestExecutionListener.class
}
)
public
class
CustomerServiceTest {
@Autowired
private
CustomerService customerService;
@Test
public
void
testFind
(
) throws
Exception {
List<
Customer>
customerList =
customerService.find
(
"hil"
);
assertEquals
(
1
, customerList.size
(
));
assertEquals
(
"Phillip"
, customerList.get
(
0
).getFirstName
(
));
}
}
Comme le test utilise le lanceur de Spring SpringJUnit4ClassRunner, nous devons définir le fichier du contexte de l'application (applicationContext.xml) dans lequel nous indiquons les Beans que Spring doit scanner, ainsi que la configuration de Hibernate pour la base de données à monter en mémoire. Le fichier est le suivant :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns
=
"http://www.springframework.org/schema/beans"
xmlns
:
xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
xmlns
:
context
=
"http://www.springframework.org/schema/context"
xmlns
:
tx
=
"http://www.springframework.org/schema/tx"
xsi
:
schemaLocation
=
"
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"
>
<
context
:
component-scan
base-package
=
"fr.soat.icoundoul.dbunit"
/>
<
tx
:
annotation-driven
transaction-manager
=
"transactionManager"
/>
<bean
id
=
"dataSource"
class
=
"org.springframework.jdbc.datasource.DriverManagerDataSource"
>
<property
name
=
"driverClassName"
value
=
"org.hsqldb.jdbcDriver"
/>
<property
name
=
"url"
value
=
"jdbc:hsqldb:mem:paging"
/>
<property
name
=
"username"
value
=
"sa"
/>
<property
name
=
"password"
value
=
""
/>
</bean>
<bean
id
=
"entityManagerFactory"
class
=
"org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
>
<property
name
=
"dataSource"
ref
=
"dataSource"
/>
<property
name
=
"jpaDialect"
>
<bean
class
=
"org.springframework.orm.jpa.vendor.HibernateJpaDialect"
/>
</property>
</bean>
<bean
id
=
"transactionManager"
class
=
"org.springframework.orm.jpa.JpaTransactionManager"
>
<property
name
=
"entityManagerFactory"
ref
=
"entityManagerFactory"
/>
</bean>
</beans>
En lançant le test, on obtient le résultat suivant :
Ce test a échoué et cela est normal, car notre base de données en mémoire ne contient aucune donnée. Pour nous assurer que notre service fonctionne, nous devons insérer des données. C'est là que DbUnit intervient. Il permet de peupler la base en utilisant un fichier XML. Appelons le sampleData.xml. Ce dernier sera chargé lors de l'exécution du test en utilisant l'annotation @DatabaseSetup. Nous devons donc créer ce fichier XML et modifier notre test comme suit :
2.
3.
4.
5.
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<Customer
id
=
"0"
title
=
"Mr"
firstName
=
"Phillip"
lastName
=
"Webb"
/>
<Customer
id
=
"1"
title
=
"Mr"
firstName
=
"Fred"
lastName
=
"Bloggs"
/>
</dataset>
Le test devient :
En relançant le test, nous obtenons le résultat attendu soit :
Le test d'insertion de service a donc bien fonctionné. Nous pouvons aussi tester une suppression de données. Pour cela, on met à jour le service CustomerService en ajoutant une méthode de suppression comme suit :
2.
3.
4.
public
void
remove
(
int
personId) {
Customer customer =
entityManager.find
(
Customer.class
, personId);
entityManager.remove
(
customer);
}
Pour tester cette méthode, nous devons utiliser l'annotation @ExpectedDatabase, qui permet de vérifier les données attendues après la suppression. Comme pour l'annotation @DatabaseSetup, nous passons en paramètre le fichier XML représentant l'état de la base après la suppression.
Dans notre test ci-dessous, nous voulons supprimer le Customer avec l'id 1. Donc, il ne devrait rester en base que le customer avec l'id 0, d'où le fichier (appelons-le expectedData.xml) à passer à l'annotation @ExpectedDatabase :
2.
3.
4.
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<Customer
id
=
"0"
title
=
"Mr"
firstName
=
"Phillip"
lastName
=
"Webb"
/>
</dataset>
Le test testRemove() est comme suit :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
@RunWith
(
SpringJUnit4ClassRunner.class
)
@ContextConfiguration
(
locations =
{
"/applicationContext.xml"
}
)
@TestExecutionListeners
({
DependencyInjectionTestExecutionListener.class
, DbUnitTestExecutionListener.class
}
)
public
class
CustomerServiceTest {
@Autowired
private
CustomerService customerService;
@Test
@DatabaseSetup
(
"/sampleData.xml"
)
public
void
testFind
(
) throws
Exception {
List<
Customer>
customerList =
customerService.find
(
"hil"
);
assertEquals
(
1
, customerList.size
(
));
assertEquals
(
"Phillip"
, customerList.get
(
0
).getFirstName
(
));
}
@Test
@DatabaseSetup
(
"/sampleData.xml"
)
@ExpectedDatabase
(
"/expectedData.xml"
)
public
void
testRemove
(
) throws
Exception {
customerService.remove
(
1
);
}
}
En lançant ce test, on obtient le résultat attendu soit :
III. Conclusion▲
Nous avons vu dans cet article que DbUnit permet d'effectuer la migration de données. Ainsi nous pouvons distribuer les données d'une base sur différentes autres bases de données, en n'injectant dans chacune des bases que les données nécessaires.
IV. Remerciements▲
Cet article a été publié avec l'aimable autorisation de la société SoatSoat.
Nous tenons à remercier milkoseck pour sa relecture orthographique et Mickael Baron pour la mise au gabarit.