Ce billet est un retour d’expérience à la suite de ma participation à la conférence VoxxedDay Luxembourg 2023. Après avoir suivi plusieurs présentations, rassurez-vous je ne vais pas vous expliquer une énième fois comment les IAs vont peut-être nous remplacer !
Je m’adresse à tous les devs Java qui ont rêvé d’implémenter des micro-services gérant exclusivement leur modèle de données de manière native. Imaginez un monde où vous pouvez vous débarrasser des langages de query des BDs. Un monde où l’on peut directement manipuler les données en Java natif sans se soucier des contraintes liées au type de BD. Un monde où chaque micro-service ne peut avoir accès aux données d’un autre domaine que par des end-points.
Microstream est un moteur de persistance pour le stockage des objets et des documents Java, respectant le motif (ou modèle) des systèmes prévalents.
Pourquoi faire cela ? Les architectures 3 tiers fonctionnent bien !
Elles permettent une séparation entre les différentes couches. Chaque niveau est facile à maintenir. Cette séparation facilite la scalabilité. Du point de vue des développeurs, chaque couche constitue un ensemble de technologies, d’outils et de savoir-faire hétérogènes dont la maintenance est complexe. Par exemple en Java, l’utilisation des scripts SQL par la couche application est naturelle. Bien que cela ajoute plus de complexité et rende le code difficilement testable.
Query( value = "SELECT * FROM USERS u WHERE u.status = 1",
nativeQuery = true)
Collection<User> findAllActiveUsersNative();
// Une erreur dans une requête SQL ne peut être détectée qu'à l'exécution; Java ne permet pas à la compilation de vérifier la syntaxe SQL sans ajouter une librairie annexe.
Avec microstream il est possible de concevoir la couche de données comme faisant partie intégrante de la logique applicative. En java cela revient à manipuler des collections d’objets persistants. Dans ce cas, le modèle de données est un graphe de documents facilement manipulable par des boucles et des filtres.
List<Book> booksFrom1998 = rootInstance.getBooks().stream()
.filter(book -> book.getYear() == 1998)
.collect(Collectors.toList());
Par exemple, dans le cas des micro-services déployés dans un cluster, la couche de données d’une application est généralement implémentée par un SGBD déployé dans un cluster, qu'il soit géré ou non. En fonction des providers, le coût peut être exorbitant comparé à un filestorage simple.
Comment cela fonctionne-t-il ?
Microstream fonctionne comme un middleware qui permet de gérer la persistance d' objets Java. Concrètement l’api storage Manager permet d’initialiser une base de donnée en précisant un répertoire sur un volume local ou distant.
EmbeddedStorageManager storageManager = EmbeddedStorage.start(/directory/);
L’objet storageManager ainsi créé offre un ensemble de méthodes pour la création de notre modèle de données.
Modèle de données
Il est considéré comme un graphe d’objets Java. Dans ce graphe, les nœuds racines (Root Instances) sont les points d’entrée pour le parcourir. Il est possible de définir plusieurs Root instances pour le même graphe.
DataRoot dataRoot = new DataRoot(name);
dataRoot.setBookList(new ArrayList<>());
storageManager.setRoot(dataRoot);
storageManager.storeRoot();
storageManager.store(dataRoot.getBookList());
Les objets sont chargés en mémoire comme une base de données mémoire. Pour éviter un volume important de données et une surcharge de la JVM, il existe des mécanismes de lazy loading par exemple. La persistance de données sur le volume est gérée de manière transparente par microstream à travers la JVM.
Sauvegarde de données
L’invocation de la méthode store du storageManager permet la création d'une base de données ou sa modification si elle existe déjà. Dans le cas d’une modification, il existe aussi un mécanisme de vérification du modèle et versionning. En effet, la définition d’une classe peut évoluer entre deux sauvegardes, dans ce cas le schéma est automatiquement mis à jour par microstream.
Chargement de données
Les objets d’une base de données sont chargés automatiquement à l’appel de la méthode start si une base de données est trouvée. Par défaut, tous les objets sont chargés en eager loading. Pour pallier un éventuel problème de mémoire dans le cas d’une BD avec des millions d’enregistrements,le moteur de microstream propose de faire du Lazy loading qui revient à charger les références des objets en mémoire.
@Lazy
private ArrayList<MyCustomClass> customClassList = new ArrayList<>();
Suppression de données
Cette opération revient à retirer un objet d’une liste Java par la méthode “remove()”
root.myArrayList.remove(0);
storage.store(root.myArrayList);
Requête des données
Le modèle de données étant NoSQL, il est relativement simple de parcourir le graphe d’objets ou de listes. En utilisant les outils Java comme les Streams, nous pouvons définir des filtres.
public List<Article> getUnAvailableArticles() {
return shop.getArticles().stream()
.filter(a -> !a.available())
.collect(Collectors.toList());
}
API REST
Il est possible d’exposer directement les objets Java dans une API REST, en incluant facilement le micro framework Spark, en incluant la dépendance microstream-storage-restservice-sparkjava.
EmbeddedStorageManager storage = EmbeddedStorage.start();
if (storage.root() == null) { storage.setRoot(new Object[] {
LocalDate.now(), X.List("a", "b", "c"), 1337 });
storage.storeRoot();
}
// create the REST service
StorageRestService service = StorageRestServiceResolver.resolve(storage);
// and start it
service.start();
Attends un peu, c'est du pipeau ton truc...
Une implémentation de micro-services sans SGBD en Java est possible de plusieurs façons. Le point positif avec microstream est de mettre en avant les points forts du langage Java dans la manipulation des données. Le développeur n’a aucune limitation lors de la définition de son modèle de données en considérant les données comme un réseau d’objets Java. Cependant, un SGBD offre une garantie sur l’intégrité et la cohérence des données.
Comment pouvons-nous gérer les transactions?
Les transactions sont respectueuses du principe ACID avec microstream. De manière générale, le langage Java en propose déjà plusieurs pour assurer la cohérence dans un environnement multi thread en java par exemple: la synchronisation (synchronized), l’utilisation des locks, les collections thread safe (CocurrentHashMap), etc.
Comment cela fonctionne dans une architecture distribuée?
Dans le cadre d’une application distribuée avec plusieurs instances d’un même pod, la cohérence des données est assurée par Microstream. Pour le faire dans le code il suffit d’utiliser la classe ClusterStorageManager.
static {
storage = new ClusterStorageManager<>(new DataRoot());
root = storage.getRoot();
}
Dans ce cas, chaque JVM de chaque pod sera géré par un pod master pour la synchronisation.
Est-il possible de migrer d’une application existante?
Dans le cas d’un micro-service utilisant JPA et une base de données Postgresql. La migration de la couche d’accès aux données est possible. Cependant, la complexité peut être importante en fonction du modèle de données. En effet, cela revient à passer du schéma relationnel à un modèle de graphe d’objets.
Quels sont les usages réalistes de mon point de vue?
Microstream apporte un nouveau paradigme de modélisation des applications en Java qui semble adapté pour des nouvelles applications avec des modèles de données graphe.
Même s’il n'y a pas de restrictions en termes de cas d'utilisation, je pense qu'il est possible de l’utiliser comme mémoire cache dans une application