Points Clés
- Premières étapes de l’utilisation de NoSQL avec Java
- Exemples avec Cassandra et MongoDB
- Le déploiement dans le Cloud
Les bases de données non relationnelles sont un sujet évoqué lorsque nous parlons de nouvelle modélisation ou de persistance polyglotte. Cependant, quels sont les impacts sur cette adoption ? Le but de cet article est de couvrir les premières étapes d'utilisation de ce type de base de données dans une architecture d'entreprise.
Il existe plusieurs articles qui traitent de ce qu'est une base de données non relationnelle, des types, etc. Cependant, dans cet article, je commencerai par aborder ce que NoSQL n'est pas :
-
No-Security : oui, quelle que soit la base de données sélectionnée, la sécurité est toujours un problème important. En d'autres termes, il est essentiel que les instances de base de données ne soient pas exposées publiquement et il est important de toujours utiliser une base de données avec un utilisateur et un mot de passe. En plus de la validation dans la base de données, le niveau logiciel doit être protégé, c'est-à-dire que le mot de passe ne doit pas être stocké directement dans le code. Dans un monde idéal, le développeur ne doit pas connaître l'utilisateur et le mot de passe de la base de données de production, tout cela grâce au troisième facteur de The Twelve Factor app. Ces sujets ne sont pas nouveaux, cependant, il convient de les mentionner car il y a des problèmes majeurs liés à ce sujet.
-
No-Data-Integrity : la ressource qui existe dans la grande majorité des bases de données non relationnelles est certainement sans schéma, cependant, cela ne signifie pas que l'intégrité des données est un point exclusif des bases de données relationnelles. Il est très important que toutes les données soient cohérentes pour l'entreprise et qu'elles soient validées à ce niveau.
-
No-responsibility : lors du choix d'une nouvelle couche physique au sein de votre architecture, il est très important de comprendre qu'elle nécessitera plusieurs activités, sans parler d'un nouveau point de complexité, qui est par conséquent, un nouveau point de risque pour l'entreprise. Après tout, nous devons le répéter comme un mantra, après le hakuna matata, l'environnement de production n'est pas un laboratoire et quelqu'un doit en être responsable.
-
No-mistakes : vous avez certainement participé à une discussion et entendu : j'ai choisi cette solution car elle me donne de l'évolutivité. À mon avis, actuellement, l'évolutivité est devenue un mot miracle, c'est-à-dire qu'elle justifie les plus grosses erreurs dans l'application. Il est important de comprendre que les solutions relationnelles sont toujours vivantes et sont toujours très importantes. C'est-à-dire pour certaines solutions, l'utilisation de NoSQL pourrait être une grosse erreur. Il n'y a pas de solutions qui prouvent des erreurs ou des mauvais choix et NoSQL en fait partie. Il est à noter que le terme «scalabilité» est très large et comporte plusieurs points d'analyse, il faut donc comprendre quel point vous gagnez, où vous perdez, l'avantage est à l'écriture ou à la lecture, verticalement ou horizontalement ? Malheureusement, la grande majorité des personnes qui utilisent ce terme ne connaîtront pas ces réponses. Enfin, comme dirait Donald Knuth, l'optimisation prématurée est la racine du mal.
-
No-trade-off : comme mentionné précédemment, NoSQL n'est pas une preuve d'échecs, de plus, ce n'est pas non plus une preuve de compromis, c'est-à-dire qu'il aura des avantages et des inconvénients et le théorème CAP vous le rappellera à tout moment.
Le monde de la persistance polyglotte peut apporter plusieurs avantages à une solution. L'erreur de modélisation peut condamner son application en performance et en charge de travail. Ce à quoi nous arrivons dans un accord standard, c'est qu'en général les bases de données non relationnelles, ou NoSQL, fonctionnent différemment des bases relationnelles. Autrement dit, au lieu de travailler avec des normalisations, nous effectuons des requêtes en fonction de ce que nous voulons qu'elles fassent.
Modélisation avec la famille orientée documents (MongoDB)
Pour parler de modélisation, nous commencerons par parler des documents en NoSQL. Il y a plusieurs astuces pour une meilleure modélisation, cependant, mes préférés sont proposées par Oren Eini dans une brillante conférence tenue en 2017 et Elemar Júnior sur son blog, en général, il mentionne que pour effectuer une modélisation il est important de prendre en compte trois caractéristiques très simples si vous connaissez déjà DDD :
- Cohérence : la capacité d'un document à être compréhensible et unique. Dans une analogie avec le relationnel, ce serait comme apporter des informations sans avoir à faire de jointures
- Indépendance : c'est un document qui a du sens d'exister même s'il est seul ou a sa propre raison d'exister
- Isolement : la modification d'une donnée ne doit pas impliquer la modification d'une autre base de données
Pour mettre cela en pratique, nous avons la modélisation suivante ci-dessous, l'exemple que nous utiliserons est le contrôle des commandes au sein d'un e-commerce. Un point important est que nous utiliserons la spécification relative à NoSQL dans le monde Jakarta ou Jakarta NoSQL, cependant, cela est applicable à tout framework de mapping comme Spring Data.
@Entity
public class Order {
@Id
private ObjectId id;
@Column
private LocalDateTime orderedAt;
@Column
private List<Product> items;
@Column
private Address shipTo;
}
@Entity
public class Product {
@Column
private String name;
@Column
@Convert(MoneyConverter.class)
private MonetaryAmount value;
}
@Entity
public class Address {
@Column
private String city;
@Column
private String country;
@Column
private String postalCode;
}
Une fois la modélisation réalisée, elle pourra être utilisée comme indiqué ci-dessous :
public class App {
public static void main(String[] args) {
try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
DocumentTemplate template = container.select(DocumentTemplate.class).get();
Address address = Address.builder()
.withCity("Salvador")
.withCountry("Brazil")
.withPostalCode("40235220")
.build();
CurrencyUnit dollar = Monetary.getCurrency(Locale.US);
Order order = new Order(address);
order.add(new Product("Pen", Money.of(1, dollar)));
order.add(new Product("Notebook", Money.of(1_000, dollar)));
order.add(new Product("Smartphone", Money.of(1_500, dollar)));
template.insert(order);
System.out.println(order);
}
}
}
Il est très intéressant de noter que la commande contient l'adresse de l'utilisateur et cela a tendance à dupliquer les informations. Cependant, que se passe-t-il si l'utilisateur change d'adresse ? Postérieurement ? Il n'est tout simplement pas nécessaire de modifier, après tout, s'il a passé une commande en 2019 avec une adresse, le fait qu'il ait une nouvelle adresse en 2020 a tendance à ne pas impacter la commande passée même s'il a passé plusieurs commandes en 2019. Nous parlons de base de données NoSQL : la dénormalisation est votre meilleur ami.
La conclusion de la modélisation pour les bases de données de type documents est qu'il est possible de le faire grâce au concept DDD.
MongoDB a des transactions; néanmoins, comme le dit sa documentation, il y a un coût élevé, et si vous l'utilisez beaucoup, cela signifie que vous n'avez pas modélisé correctement.
Modélisation avec la famille orientée colonnes (Cassandra)
Quittons une base de données de la famille orientée documents et aller dans une base de la famille orientée colonnes, dans notre cas : Cassandra. Il est important, tout d'abord, de comprendre Cassandra et les possibilités de requêtes plus limitées qu'une base de données orientée documents et une base relationnelle. Comprendre Cassandra est important, principalement, pour éviter les erreurs les plus classiques : load balancer et constater que le fonctionnement d'une clé composite dans Cassandra est identique à celui du relationnel.
Une fois que nous comprenons le fonctionnement d’une base Cassandra, nous pouvons passer à la modélisation. Au sein de Cassandra, il y a deux objectifs : répartir les données entre les clusters et minimiser le nombre de partitions lues, c'est-à-dire une requête qui renvoie tout ce qui est nécessaire dans l'entreprise. Notez que ces deux règles nécessitent un certain équilibre, après tout, mettre toutes les informations dans un seul cluster enfreindrait la première règle qui est de répartir les données entre les clusters.
En termes de capacités de recherche, il est important de comprendre que les requêtes ont tendance à fonctionner dans l'ordre suivant :
- Dans le monde idéal, recherchez à l'aide de la clé
- Evitez autant que possible les index secondaires, en général, la création de cette ressource se fait à quelques exceptions près
- N'utilisez jamais la fonction de allow-filtering. Ce type de recherche est effectué à partir de la mémoire et de manière inefficace, il est fortement recommandé qu'une telle ressource ne soit pas utilisée dans un environnement de production
Pour faciliter votre modélisation dans Cassandra basée sur une modélisation orientée requêtes, Cassandra utilise certains types spéciaux :
- Set : similaire à Set en Java qui permet l'insertion d'une seule fois l'élément
- List : similaire à List en Java, il est possible de placer un élément non unique dans la liste. Une fois que l'ordre est important, la liste finit par avoir l'effet de lecture avant écriture, donc, autant que possible, utilisez Set
- Map : similaire à Map dans le monde Java
- UDT : un type personnalisable dans Cassandra
La modélisation avec Cassandra sera illustrée par trois scénarios:
Le premier sera une simple liste de contacts.
N'oubliez pas que Cassandra doit exécuter la requête pour créer la famille de colonnes avant d'insérer les données.
Dans ce cas, nous aurons la requête de création suivante:
CREATE KEYSPACE IF NOT EXISTS samples WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};
CREATE TYPE IF NOT EXISTS samples.address(city text, country text, postalCode text);
CREATE COLUMNFAMILY IF NOT EXISTS samples.Contact ("_id" text PRIMARY KEY, birthday date, details map<text, text>, address address);
Au sein de la modélisation, nous aurons:
@Entity
class Contact {
@Id
private String name;
@Column
private LocalDate birthday;
@Column
private Map<String, String> details;
@UDT("address")
@Column
private Address address;
}
@Entity
class Address {
@Column
private String city;
@Column
private String country;
@Column("postalcode")
private String postalCode;
}
Ainsi, il est possible de voir le code fonctionner, à nouveau, dans une application Java SE :
import jakarta.nosql.column.ColumnQuery;
import org.eclipse.jnosql.artemis.cassandra.column.CassandraTemplate;
import javax.enterprise.inject.se.SeContainer;
import javax.enterprise.inject.se.SeContainerInitializer;
import java.time.LocalDate;
import static jakarta.nosql.column.ColumnQuery.select;
import static java.time.Month.MARCH;
public class ContactApp {
public static void main(String[] args) {
try(SeContainer container = SeContainerInitializer.newInstance().initialize()) {
CassandraTemplate template = container.select(CassandraTemplate.class).get();
Address address = Address.builder()
.withCity("Sao Paulo")
.withCountry("Brazil")
.withPostalCode("01312001")
.build();
Contact contact = Contact.builder()
.withAddress(address)
.withBirthday(LocalDate.of(1992, MARCH, 27))
.withName("Poliana").build();
contact.put("twitter", "twitter");
contact.put("phone", "123456789");
contact.put("facebook", "poliana_facebook");
template.insert(contact);
System.out.println(contact);
ColumnQuery query = select().from("Contact").where("_id").eq("Poliana").build();
template.select(query).forEach(System.out::println);
}
}
}
Un autre exemple serait l'immatriculation d’une voiture qui contiendrait des informations telles que la plaque d'immatriculation, la ville, la couleur et certaines informations du propriétaire. En tenant compte du fait que chaque fois que je recherche la voiture elle retourne le propriétaire, il est possible d'utiliser l'UDT pour représenter le propriétaire de la voiture, par exemple.
Dans ce cas, la requête de création serait :
CREATE KEYSPACE IF NOT EXISTS samples WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};
CREATE TYPE IF NOT EXISTS samples.owner(name text, license text);
CREATE COLUMNFAMILY IF NOT EXISTS samples.Car ("_id" text PRIMARY KEY, city text, color text, owner owner);
Au sein de la modélisation, nous aurons:
@Entity
class Car {
@Id
private String plate;
@Column
private String city;
@Column
private String color;
@UDT("owner")
@Column
private Owner owner;
}
@Entity
class Owner {
@Column
private String name;
@Column
private String license;
}
Pour le troisième et dernier exemple de modélisation avec Cassandra, nous stockerons des recettes, chaque recette aura les informations de base en plus de ses ingrédients, chaque l'ingrédient possède un nom, une quantité et une unité de mesure.
Dans ce cas, la requête de création sera :
CREATE KEYSPACE IF NOT EXISTS samples WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};
CREATE TYPE IF NOT EXISTS samples.ingredient(name text, quantity decimal, unit text);
CREATE COLUMNFAMILY IF NOT EXISTS samples.Recipe ("_id" text PRIMARY KEY, city text, ingredients set<frozen<ingredient>>);
Au sein de la modélisation, nous aurons:
@Entity
public class Recipe {
@Id
private String name;
@Column
private String city;
@Column
@UDT("ingredient")
private Set<Ingredient> ingredients;
}
@Entity
public class Ingredient {
@Column
private String name;
@Column
private BigDecimal quantity;
@Column
private String unit;
}
Ainsi, notre code en cours d'exécution sera comme indiqué ci-dessous, en rappelant que pour éviter l'effet de lecture avant écriture dans la liste, nous utilisons le type Set.
import jakarta.nosql.column.ColumnQuery;
import org.eclipse.jnosql.artemis.cassandra.column.CassandraTemplate;
import javax.enterprise.inject.se.SeContainer;
import javax.enterprise.inject.se.SeContainerInitializer;
public class RecipeApp {
public static void main(String[] args) {
try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
CassandraTemplate template = container.select(CassandraTemplate.class).get();
Recipe recipe = new Recipe("Bauru", "Bauru");
recipe.add(new Ingredient("French bun with crumb", 1D, "unit"));
recipe.add(new Ingredient("Cheese", 300D, "grams"));
recipe.add(new Ingredient("Roast beef", 500D, "grams"));
recipe.add(new Ingredient("Pickled cucumber", 150D, "grams"));
recipe.add(new Ingredient("Tomato", 100D, "grams"));
template.insert(recipe);
ColumnQuery query = ColumnQuery.select().from("Recipe").where("_id").eq("Bauru").build();
template.select(query).forEach(System.out::println);
}
}
}
Faciliter la maintenance et ne pas réinventer la roue dans les clouds
En plus de la modélisation et des conseils sur ce qui n'est pas une base de données non relationnelle, il est important de parler de la maintenance de la base de données elle-même. Dans une vision architecturale et à partir du choix du type de services, nous aurons des compromis. Par exemple, kubernetes est sans doute devenu assez populaire dans le monde aujourd'hui, cependant, il existe plusieurs rapports de complexité dans sa maintenance et rappelez-vous que la complexité a tendance à entraîner des risques. Encore une bonne discussion sur Docker et la base de données et, en général, il y a quelques recommandations pour ne pas l'utiliser dans l'environnement de production. Sans parler d'autres points, par exemple, la sauvegarde des données.
Pour faciliter la maintenance de ces types de bases de données, il est également possible de faire une base de données en tant que service DBaaS dans laquelle toutes les opérations, maintenance, sauvegarde sont effectuées par le fournisseur. Ainsi, il n'y a pas lieu de s'inquiéter de cette complexité. Exemples:
- Cassandra no Google Cloud : une solution Cassandra as a service fournie par Google en partenariat avec DataStax
- Aiven : une solution multi-cloud pour Cassandra
- DataStax Astra : se concentre sur le concept de création de Cassandra en tant que service basé sur le cloud native
- MongoDB Atlas : une solution multi-cloud pour les bases de données MongoDB
- Platform.sh : une solution de plateform as a service qui, en plus de maintenir l'application, a accès à divers services tels que MongoDB, PostgreSQL, MariaDB, entre autres
Avec cela, nous avons parlé des concepts de base pour commencer à utiliser des bases de données non relationnelles dans une architecture d'entreprise avec Java. Nous avons commencé par définir ce que n'est pas une base de données NoSQL, en modélisant pour enfin mentionner qu'il n'y a pas de problème à utiliser le concept de DBaaS pour réduire les risques du côté architectural. Un point intéressant est qu'il existe des fonctionnalités très intéressantes comme la validation de bean et un mappeur qui doivent être utilisés avec parcimonie.
Cependant, il convient de souligner qu'il y aura toujours une impédance de paradigme, c'est-à-dire qu'il y a des choses dans l'orientation objet qu'il ne sera pas possible d'appliquer à une base de données et de l'utiliser avec parcimonie. Et nous avons finis par parler un peu des outils et ils sont discutés de manière très forte au sein des conférences. Les conférences sont d'excellents points pour apprendre et se familiariser avec les nouveaux outils, cependant, il convient de noter que les conférences ne reflètent souvent pas la réalité, après tout, une application en production a beaucoup plus d'exigences qu'une application qui doit durer 50 minutes.
L'exemple de code: https://github.com/soujava/nosql-design-pitfalls
A propos de l'auteur
Otávio Santana est un ingénieur logiciel avec une vaste expérience dans le développement open source, avec plusieurs contributions à JBoss Weld, Hibernate, Apache Commons et à d'autres projets. Axé sur le développement multilingue et les applications haute performance, Otávio a travaillé sur de grands projets dans les domaines de la finance, du gouvernement, des médias sociaux et du commerce électronique. Membre du comité exécutif du JCP et de plusieurs groupes d'experts JSR, il est également un champion Java et a reçu le JCP Outstanding Award et le Duke's Choice Award.