Télécharger des images depuis et vers une base de données en utilisant JPA

On ne trouve pas beaucoup d’aide en français lorsqu’on veut enregistrer des images en base de données avec Java. Voici donc une petite application qui va lire une collection de fichiers-images pour les stocker dans une base de données.

Prérequis

Tout d’abord, il me faut un dossier contenant des images. Ça tombe bien, mon dossier « C:\Images\A Trier\ » en est plein.
Ensuite, je ne m’éterniserai pas sur l’architecture du projet et ses dépendances. En principe il fonctionne avec n’importe quelle base de données et n’importe quelle implémentation de JPA. Personnellement, je vais le faire en même temps que je rédige cet article en utilisant MySql5 et Hibernate 3.

L’entité « Image »

Les attributs de l’image sont :

  • name : son nom (le nom du fichier sans l’extension)
  • image : l’image sérialisée. Pour faire simple, ce sera un tableau d’octets mais il y a des classes Java plus appropriées, notamment si vous souhaitez ensuite afficher les images avec Swing ou les modifier via du code Java…
  • mimeType : je vais revenir là-dessus, mais c’est le type Mime de l’image. Dans notre cas il va permettre d’ajouter l’extension du fichier, mais vue qu’on parle de sérialisation, il sera indispensable quelle que soit votre application par la suite.
Classe MonImage

Voici la partie annotée de la classe MonImage :

1
2
3
4
5
6
7
8
9
10
11
@Entity
@Table(name="images")
public class monImage implements Serializable{
    @Id
    private String name;
    @Lob()
    private byte[] image;
    @Column(name="mime_type")
    private String mimeType;
    //getters, setters, hashcode et equals...
}

Ici, le nom de l’image est sa clé primaire, mais libre à vous de définir une clé primaire différente.
L’image sérialisée sera stockée dans un LOB (Large Object) en base de données. Chaque éditeur de base de données fournit ses propres formats et l’implémentation de JPA choisira celui qui convient. Nous, tout ce qu’on a à faire, c’est écrire l’annotation « @Lob() ».

Ensuite, nous allons rajouter un setter un peu spécial, qui permet de passer une image bufferisée lors de la construction d’une instance de cette classe. Cela sera plus convenable que de lui passer directement un tableau d’octets. D’autant plus que, dans ce tutoriel nous allons lire des fichiers, mais dans une application client/serveur, vous aurez certainement à lire une requête… Voici ce setter :

1
2
3
4
5
6
7
public void setImage(BufferedImage bufferedImage) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ImageIO.write(bufferedImage, mimeType.split("image/")[1], baos);
    baos.flush();
    this.image = baos.toByteArray();
    baos.close();
}

Réciproquement, nous n’auront pas besoin du getter puisque nous allons pouvoir écrire directement un tableau d’octets dans n’importe quel flux Java. Mais voici quand même son code en bonus :

1
2
3
public BufferedImage getBufferedImage() throws IOException {
    return ImageIO.read(new ByteArrayInputStream(image));
}

Enfin, je vous propose un dictionnaire statique des extensions de fichiers qui seront associées aux types mime gérés par notre petit programme. Libre à vous de choisir les vôtres :

1
2
3
4
5
6
7
8
9
transient public static final Map acceptedMimeType = new HashMap();
static{
    acceptedMimeType.put("jpg", "image/jpeg");
    acceptedMimeType.put("gif", "image/gif");
    acceptedMimeType.put("png", "image/png");
    acceptedMimeType.put("tif", "image/tiff");
    acceptedMimeType.put("svg", "image/svg+xml");
    acceptedMimeType.put("ico", "image/vnd.microsoft.icon");
}

Le DAO

Voici un petit DAO qui pourra être réutilisé ailleurs. Il est très simple et permet de charger/décharger des donnése dans une base de données via JPA :

1
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
public class GenericDao {  
    private static final String PERSISTENCE_UNIT_NAME = "persistence-config";
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory(PERSISTENCE_UNIT_NAME);
    private static EntityManager em = emf.createEntityManager();
    /**
     * Enregistre une collection d'objets mappés via des annotations JPA
     * @param data
     */

    public static void persist(Collection collection){
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        for (Serializable serializable : collection) {
            em.persist(serializable);
        }
        tx.commit();
    }    
    /**
     * Enregistre un objet mappé via des annotations JPA
     * @param data
     */

    public static void persist(Serializable data){
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        em.persist(data);
        tx.commit();
    }
     /**
     * Retourne toutes les instances d'une classe donnée
     * @param className : le nom d'une classe annotée par JPA
     * @return
     */

    @SuppressWarnings("rawtypes")
    public static List load(String className){
        Query q = em.createQuery("from " + className);
        return q.getResultList();
    }
   
}

Je ne détaille pas le contenu du fichier persistence.xml, mais dans le code ci-dessus, la constante PERSISTENCE_UNIT_NAME doit avoir la même valeur que la balise persistence-unit. Par exemple, ici :

<persistence-unit name="persistence-config" >

Sérialisation/Désérialisation des images

Nous allons développez une classe de service « ImageSerializator » qui va fournir les services de sérialisation/désérialisation d’images via des méthodes statiques.

Voici une première méthode statique qui crée une instance de la classe MonImage à partir d’un fichier image (fichier .jpg, par exemple) :

1
2
3
4
5
6
7
public static MonImage serializeImage(File imageFile) throws IOException {
    MonImage monImage = new MonImage();
    monImage.setName(imageFile.getName().substring(0, imageFile.getName() .lastIndexOf(".")));
    monImage.setMimeType(MonImage.acceptedMimeType.get(imageFile.getName().substring(imageFile.getName() .lastIndexOf(".")+1)));
    monImage.setImage(ImageIO.read(imageFile));
    return monImage;
}

Le nom de l’image est le nom du fichier sans l’extension, son type mime est la valeur correspondante à l’extension dans le dictionnaire statique que nous avons créé plus haut, et on utilise le setter spécial pour stocker l’image dans un tableau d’octets.

Réciproquement, voici la méthode qui permet d’écrire une image sérialisée dans un fichier du répertoire passé en paramètre :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void writeFileIntoFolder(MonImage monImage, File folder) throws IOException{
    String extension = getExtension(monImage.getMimeType());
    File file = new File(folder + "/" + monImage.getName() + "." + extension);
    file.createNewFile();
    FileImageOutputStream fos = new FileImageOutputStream(file);
    fos.write(monImage.getImage());
    fos.close();
}
/**
 * Récupère l'extension du fichier en fonction du type mime dans le dictionnaire des formats d'image acceptés
 * @param mimeType
 * @return
 */

private static String getExtension(String mimeType) {
    for (Entry typeMimeEntry : MonImage.acceptedMimeType.entrySet()) {
        if(mimeType.equals(typeMimeEntry.getValue())){
            return typeMimeEntry.getKey();
        }
    }
    return null;
}

Cette méthode suppose que le répertoire passé en paramètre existe. Elle va créer un fichier dont le nom sera l’attribut « name » de l’image sérialisée passée en paramètre, et l’extension correspondra au type mime définit dans le dictionnaire (ex : « .jpg » si le type mime est « image/jpeg »).

Enfin, on rajoute les deux méthodes de traitement par lot ci-dessous :

1
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
/**
 * Crée plusieurs fichiers images à partir d'une liste d'images sérialisées, dans le dossier passé en paramètre
 */

public static void writeFilesIntoFolder(List images, File folder) throws IOException{
    for (MonImage imageMudu : images) {
        writeFileIntoFolder(imageMudu, folder);
    }
}
/**
 * Sérialise l'ensemble des images contenues dans le répertoire passé en paramètre
 */

public static List serializeFolder(File folder) throws IOException {
    List images = new ArrayList();
    //Filtre pour ne garder que les fichiers images acceptés
    FileFilter imageFileFilter = new FileFilter() {
        @Override
        public boolean accept(File file) {
            String fileName = file.getName();
            //on extrait l'extension du fichier puis on vérifie qu'elle existe dans le dictionnaire
            String extension = fileName.substring(fileName.lastIndexOf(".")+1);
            if(extension == null || extension.isEmpty()) return false;
            else return MonImage.acceptedMimeType.containsKey(extension);
        }
    };
    for (File imageFile : folder.listFiles(imageFileFilter)) {
        MonImage image = serializeImage(imageFile);
        if(image != null){
            images.add(image);
        }
    }
    return images;
}

Points d’entrée du programme

A ce stade, on a quasiment terminé, il ne reste plus qu’à coder les points d’entrée du programme : un pour sérialiser les images d’un répertoire vers la base de données, et un pour faire l’inverse.
Voici le premier :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ImagesFromFolder2Database {
    public static void main(String[] args) {
        //Permet d'avoir des chemins de types "C:\Mes Documents",
        //sinon les espace seront considérés comme des séparateurs
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i &lt; args.length; i++) {
            sb.append(args[i]).append(&quot; &quot;);
        }
        File folder = new File(sb.toString());     
        //charger les images depuis le repertoire
        List images = null;
        try {
            images = ImageSerializator.serializeFolder(folder);
        } catch (IOException e) {
            e.printStackTrace();
        }      
        //enregistrement en base
        GenericDao.persist(images);
    }

A lancer après compilation avec la commande :

java ImagesFromFolder2Database.class C:\Images\A Trier\

Et voici le second :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ImageFromDataBase2Folder {
    public static void main(String[] args) {
        //Permet d'avoir des chemins de types "C:\Mes Documents",
        //sinon les espace seront considérés comme des séparateurs
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i &lt; args.length; i++) {
            sb.append(args[i]).append(&quot; &quot;);
        }
        File folder = new File(sb.toString());     
        //Chargement de la liste d&#039;images sérialisées en base de données
        @SuppressWarnings(&quot;unchecked&quot;)
        List images = GenericDao.load("MonImage");
        try {
            //Créer le répertoire si il n'existe pas
            if(!folder.exists()){
                folder.mkdir();
            }
            //Ecrire les fichiers
            ImageSerializator.writeFilesIntoFolder(images, folder);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

A lancer après compilation avec la commande :

java ImageFromDataBase2Folder.class C:\Images\A Trier\

Conclusion

Voilà, j’espère que ce code sera utile à un maximum de développeurs. Si quelque chose ne fonctionne pas -ou si tout fonctionne-, n’hésitez pas à me laisser un petit commentaire pour améliorer ce post.

Une réflexion au sujet de « Télécharger des images depuis et vers une base de données en utilisant JPA »

  1. fxrobin

    Bonjour,

    sur certaines base de données, il vaut mieux spécifier une taille pour le @Lob afin qu’il choisisse le bon type.

    Pour des images, je mets toujours une très grande valeur :

    @Column(size=Integer.MAX_VALUE)
    @Lob
    byte[] monImage;

    Sinon « GenericDao » pourraît être faite avec de generics en plus qui éviterait les suppress-warning rawtype ainsi que les « Serializable ».

    Bon article.

Les commentaires sont fermés.