Fermer

août 9, 2018

Prise en charge de Java et MongoDB 4.0 pour les transactions ACID multi-documents –


Cet article a été publié à l'origine sur MongoDB. Nous vous remercions de soutenir les partenaires qui rendent SitePoint possible.

MongoDB 4.0 ajoute le support pour les transactions ACID multi-documents.

Mais attendez … Cela signifie-t-il que MongoDB ne supportait pas les transactions? En fait, MongoDB a toujours pris en charge les transactions sous la forme de transactions à document unique. MongoDB 4.0 étend ces garanties transactionnelles sur plusieurs documents, plusieurs déclarations, plusieurs collections et plusieurs bases de données. À quoi servirait une base de données sans garantie d'intégrité des données transactionnelles?

Avant de plonger dans cet article, vous pouvez trouver tout le code et essayer les transactions ACID multi-documents ici .

Démarrage rapide

Étape 1: Démarrez MongoDB

Démarrez un nœud unique MongoDB ReplicaSet dans la version 4.0.0 minimum sur localhost, port 27017.

Si vous utilisez Docker:

  • Vous pouvez utiliser start- mongo.sh .
  • Lorsque vous avez terminé, vous pouvez utiliser stop-mongo.sh .
  • Si vous souhaitez vous connecter à MongoDB avec le Mongo Shell, vous pouvez utiliser connect-mongo.sh .

Si vous préférez démarrer manuellement mongod:

  • mkdir / tmp / data && mongod --dbpath / tmp / data --replSet rs [19659010] mongo --eval 'rs.initiate ()'

Étape 2: Démarrage de Java

Cette démo contient deux programmes principaux: ChangeStreams.java et Transactions.java . [19659009] Change Steams vous permet d'être averti de toute modification de données dans une collection ou une base de données MongoDB.

  • Le processus de transaction est la démo elle-même.
  • Vous avez besoin de deux shells pour les exécuter.

    Si vous utilisez Docker:

    Premier shell:

     ./ compile-docker.sh
    ./change-streams-docker.sh
    

    Deuxième shell:

     ./ transactions-docker.sh
    

    Si vous n'utilisez pas Docker, vous devrez installer Maven 3.5.X et un JDK 10 (ou JDK 8 minimum, mais vous devrez mettre à jour les versions de Java dans le fichier pom.xml):

    Premier shell:

     ./ compile.sh
    ./change-streams.sh
    

    Deuxième coque:

     ./ transactions.sh
    

    Comparons nos transactions à document unique existantes avec les transactions multi-documents conformes à ACID de MongoDB 4.0 et voyons comment nous pouvons tirer parti de cette nouvelle fonctionnalité avec Java.

    Avant MongoDB 4.0

    Même dans MongoDB 3.6 et versions antérieures, chaque opération d'écriture est représentée sous la forme d'une transaction portée au niveau d'un document individuel dans la couche de stockage. Étant donné que le modèle de document regroupe des données associées qui seraient autrement modélisées sur des tables parent-enfant distinctes dans un schéma tabulaire, les opérations à document unique atomique de MongoDB fournissent une sémantique de transaction répondant aux besoins d’intégrité des données de la plupart des applications. L'opération d'écriture modifiant plusieurs documents se produit en réalité dans plusieurs transactions indépendantes: une pour chaque document.

    Prenons un exemple avec une application de gestion des stocks très simple.

    Tout d'abord, j'ai besoin d'un jeu de répliques MongoDB, donc suivez les instructions ci-dessus pour démarrer MongoDB.

    Insérons maintenant les documents suivants dans un produit :

     MongoDB Enterprise rs : PRIMARY> db.product.insertMany ([
        { "_id" : "beer", "price" : NumberDecimal("3.75"), "stock" : NumberInt(5) }, 
        { "_id" : "wine", "price" : NumberDecimal("7.5"), "stock" : NumberInt(3) }
    ])
    

    Imaginons qu'il y ait une vente et que nous souhaitons offrir à nos clients une réduction de 20% sur tous nos produits.

    Mais avant d'appliquer cette réduction, nous souhaitons surveiller ces opérations dans MongoDB avec Change Streams.

    Exécutez la commande suivante dans Mongo Shell:

     cursor = db.product.watch ([{$match: {operationType: "update"}}]);
    while (! cursor.isExhausted ()) {
      if (cursor.hasNext ()) {
        print (tojson (cursor.next ()));
      }
    }
    

    Gardez ce shell sur le côté, ouvrez un autre shell Mongo et appliquez la remise:

     PRIMARY> db.product.updateMany ({}, {$ mul: {price: 0.8}})
    {"reconnu": true, "matchedCount": 2, "modifiedCount": 2}
    PRIMARY> db.product.find (). Pretty ()
    {
        "_id": "bière",
        "price": NumberDecimal ("3.00000000000000000"),
        "stock": 5
    }
    {
        "_id": "vin",
        "price": NumberDecimal ("6.0000000000000000"),
        "stock": 3
    }
    

    Comme vous pouvez le voir, les deux documents ont été mis à jour avec une seule ligne de commande mais pas en une seule transaction.
    Voici ce que nous pouvons voir dans le shell Change Stream:

     {
        "_id": {
            "_data": "825B4637290000000129295A1004374DC58C611E4C8DA4E5EDE9CF309AC5463C5F6964003C62656572000004"
        },
        "operationType": "update",
        "clusterTime": horodatage (1531328297, 1),
        "ns": {
            "db": "test",
            "coll": "produit"
        },
        "documentKey": {
            "_id": "bière"
        },
        "updateDescription": {
            "updatedFields": {
                "price": NumberDecimal ("3.00000000000000000")
            },
            "removedFields": [ ]
        }
    }
    {
        "_id": {
            "_data": "825B4637290000000229295A1004374DC58C611E4C8DA4E5EDE9CF309AC5463C5F6964003C77696E65000004"
        },
        "operationType": "update",
        "clusterTime": horodatage (1531328297, 2),
        "ns": {
            "db": "test",
            "coll": "produit"
        },
        "documentKey": {
            "_id": "vin"
        },
        "updateDescription": {
            "updatedFields": {
                "price": NumberDecimal ("6.0000000000000000")
            },
            "removedFields": [ ]
        }
    }
    

    Comme vous pouvez le constater, les temps de cluster (voir la clé clusterTime ) des deux opérations sont différents: les opérations ont eu lieu au cours de la même seconde mais le compteur de l'horodatage a été incrémenté de un. ] Ainsi, chaque document est mis à jour un par un et même si cela se produit très rapidement, quelqu'un d'autre peut lire les documents pendant l'exécution de la mise à jour et ne voir qu'un des deux produits avec la remise.

    c'est quelque chose que vous pouvez tolérer dans votre base de données MongoDB car, dans la mesure du possible, nous essayons d'intégrer des données étroitement liées ou connexes dans le même document.
    En conséquence, deux mises à jour sur le même document se produisent dans une même transaction :

     PRIMARY> db.product.update ({_ id: "wine"}, {$ inc: {stock: 1}, $ set: {description: "C'est le meilleur vin sur terre"}})
    WriteResult ({"nMatched": 1, "nUpserted": 0, "nModified": 1})
    PRIMARY> db.product.findOne ({_ id: "wine"})
    {
        "_id": "vin",
        "price": NumberDecimal ("6.0000000000000000"),
        "stock": 4,
        "description": "C'est le meilleur vin sur terre"
    }
    

    Cependant, vous ne pouvez parfois pas modéliser toutes vos données associées dans un seul document, et il existe de nombreuses raisons valables de choisir de ne pas incorporer de documents.

    MongoDB 4.0 avec des transactions ACID multi-documents

    Multi Les transactions ACID -document dans MongoDB sont très similaires à celles que vous connaissez probablement déjà dans les bases de données relationnelles traditionnelles.

    Les transactions de MongoDB constituent un ensemble conversationnel d'opérations connexes qui doivent être validées ou entièrement annulées. Les transactions permettent de s'assurer que les opérations sont atomiques, même sur plusieurs collections ou bases de données. Ainsi, avec les lectures d'isolement de snapshot, un autre utilisateur ne peut voir que toutes les opérations ou aucune d'entre elles.

    Ajoutons maintenant un panier à notre exemple.

    Pour cet exemple, 2 collections sont requises car nous avons affaire à 2 différentes entités commerciales: la gestion des stocks et le panier que chaque client peut créer lors de ses achats. Le cycle de vie de chaque document de ces collections est différent.

    Un document de la collection de produits représente un article que je vends. Cela contient le prix actuel du produit et le stock actuel. J'ai créé un POJO pour le représenter: Product.java.

     {"_id": "beer", "price": NumberDecimal ("3"), "stock": NumberInt (5)}
    

    Un panier d'achat est créé lorsqu'un client ajoute son premier article dans le panier et qu'il est supprimé lorsque le client passe à la caisse ou quitte le site Web. J'ai créé un POJO pour le représenter: Cart.java.

     {
        "_id": "Alice",
        "items": [
            {
                "price" : NumberDecimal("3"),
                "productId" : "beer",
                "quantity" : NumberInt(2)
            }
        ]
    }
    

    Le défi réside ici dans le fait que je ne peux pas vendre plus que ce que je possède: si je dois vendre 5 bières, je ne peux pas distribuer plus de 5 bières sur les différents chariots clients.

    pour vous assurer que l'opération de création ou de mise à jour du panier client est atomique avec la mise à jour du stock. C'est là que la transaction multi-document entre en jeu.
    La transaction doit échouer dans le cas où quelqu'un essaie d'acheter quelque chose que je n'ai pas dans mon stock. Je vais ajouter une contrainte sur le stock de produit:

     db.createCollection ("product", {
       validateur: {
          $ jsonSchema: {
             bsonType: "objet",
             requis: [ "_id", "price", "stock" ],
             Propriétés: {
                _id: {
                   bsonType: "string",
                   description: "doit être une chaîne et est requis"
                },
                prix: {
                   bsonType: "décimal",
                   minimum: 0,
                   description: "doit être un nombre décimal positif et est requis"
                },
                Stock: {
                   bsonType: "int",
                   minimum: 0,
                   description: "doit être un entier positif et est requis"
                }
             }
          }
       }
    })
    

    Nœud qui est déjà inclus dans le code Java.

    Pour surveiller notre exemple, nous allons utiliser les flux de modifications MongoDB introduits dans MongoDB 3.6.

    Dans chacun des threads de ce processus appelé ChangeStreams.java je vais surveiller l'une des 2 collections et imprimer chaque opération avec l'heure de cluster associée.

     // package and imports
    
    classe publique ChangeStreams {
    
        final statique privé Bson filterUpdate = Filters.eq ("operationType", "update");
        final statique privé Bson filterInsertUpdate = Filters.in ("operationType", "insert", "update");
        private static final String jsonSchema = "{$ jsonSchema: {bsonType: " objet  ", requis: [ "_id", "price", "stock" ]propriétés: {_id: {bsonType: " string  ", description: " doit être une chaîne et est required  "}, prix: {bsonType: " decimal  ", minimum: 0, description: " doit être un nombre décimal positif et est requis  "}, stock: {bsonType: " int  ", minimum: 0 , description:  "doit être un entier positif et est requis "}}}} ";
    
        main statique principale vide (String [] args) {
            MongoDatabase db = initMongoDB (args [0]);
            MongoCollection  cartCollection = db.getCollection ("cart", Cart.class);
            MongoCollection  productCollection = db.getCollection ("product", Product.class);
            ExecutorService Executor = Executors.newFixedThreadPool (2);
            executor.submit (() -> watchChangeStream (productCollection, filterUpdate));
            executor.submit (() -> watchChangeStream (cartCollection, filterInsertUpdate));
            ScheduledExecutorService programmé = Executors.newSingleThreadScheduledExecutor ();
            schedule.scheduleWithFixedDelay (System.out :: println, 0, 1, TimeUnit.SECONDS);
        }
    
        private static void watchChangeStream (collection MongoCollection >filtre Bson) {
            System.out.println ("Watching" + collection.getNamespace ());
            List  pipeline = Collections.singletonList (Aggregates.match (filtre));
            collection.watch (pipeline)
                      .fullDocument (FullDocument.UPDATE_LOOKUP)
                      .forEach ((Consumer <ChangeStreamDocument >>) doc -> System.out.println (
                              doc.getClusterTime () + "=>" + doc.getFullDocument ()));
        }
    
        MongoDatabase statique privé initMongoDB (String mongodbURI) {
            getLogger ("org.mongodb.driver"). setLevel (Level.SEVERE);
            CodecRegistry providers = fromProviders (PojoCodecProvider.builder (). Register ("com.mongodb.models"). Build ());
            CodecRegistry codecRegistry = fromRegistries (MongoClient.getDefaultCodecRegistry (), fournisseurs);
            Options MongoClientOptions.Builder = new MongoClientOptions.Builder (). CodecRegistry (codecRegistry);
            MongoClientURI uri = new MongoClientURI (mongodbURI, options);
            Client MongoClient = new MongoClient (uri);
            MongoDatabase db = client.getDatabase ("test");
            db.drop ();
            db.createCollection ("cart");
            db.createCollection ("product", productJsonSchemaValidator ());
            renvoyer db;
        }
    
        static statique CreateCollectionOptions productJsonSchemaValidator () {
            renvoie new CreateCollectionOptions (). validationOptions (
                    nouveau ValidationOptions (). validationAction (ValidationAction.ERROR) .validator (BsonDocument.parse (jsonSchema)));
        }
    }
    

    Dans cet exemple, nous avons 5 bières à vendre.
    Alice veut acheter 2 bières, mais nous n'utiliserons pas les nouvelles transactions multi-documents MongoDB 4.0 pour cela. Nous observerons deux opérations dans les flux de changements: une création du panier et une mise à jour du stock à deux moments différents.

    Ensuite, Alice ajoute 2 autres bières dans son panier et nous allons utiliser une transaction cette fois. Le résultat dans le flux de modifications sera 2 opérations se produisant à la même heure de cluster.

    Enfin, elle essaiera de commander 2 bières supplémentaires, mais le validateur jsonSchema échouera à la mise à jour du produit et entraînera une restauration. Nous ne verrons rien dans le flux de modifications.
    Voici le code source de Transaction.java :

     // package and import
    
    Transactions de classe publique {
    
        client MongoClient statique privé;
        MongoCollection statique statique  cartCollection;
        MongoCollection statique statique  productCollection;
    
        final privé BigDecimal BEER_PRICE = BigDecimal.valueOf (3);
        final privé String BEER_ID = "beer";
    
        final privé Bson stockUpdate = inc ("stock", -2);
        final final filtreId Bson = eq ("_ id", BEER_ID);
        final final Bson filterAlice = eq ("_ id", "Alice");
        final final privé Bson matchBeer = elemMatch ("items", eq ("productId", "beer"));
        final privé Bson incrementBeers = inc ("items. $. quantité", 2);
    
        main statique principale vide (String [] args) {
            initMongoDB (args [0]);
            nouvelles transactions (). demo ();
        }
    
        privé statique initMongoDB (String mongodbURI) {
            getLogger ("org.mongodb.driver"). setLevel (Level.SEVERE);
            CodecRegistry codecRegistry = fromRegistries (MongoClient.getDefaultCodecRegistry (), fromProviders (
                    PojoCodecProvider.builder (). Register ("com.mongodb.models"). Build ()));
            Options MongoClientOptions.Builder = new MongoClientOptions.Builder (). CodecRegistry (codecRegistry);
            MongoClientURI uri = new MongoClientURI (mongodbURI, options);
            client = new MongoClient (uri);
            MongoDatabase db = client.getDatabase ("test");
            cartCollection = db.getCollection ("cart", Cart.class);
            productCollection = db.getCollection ("product", Product.class);
        }
    
        démo vide privée () {
            clearCollections ();
            insertProductBeer ();
            printDatabaseState ();
            System.out.println ("######### NO TRANSACTION #########");
            System.out.println ("Alice veut 2 bières.");
            System.out.println ("Nous devons créer un panier dans la collection" panier "et mettre à jour le stock dans la collection" produit ".");
            System.out.println ("Les 2 actions sont corrélées mais ne peuvent pas être exécutées sur la même heure de cluster");
            System.out.println ("Toute erreur bloquant une opération peut entraîner une erreur de stock ou une vente de bière dont nous ne sommes pas propriétaires.");
            System.out.println ("------------------------------------------- -------------------------------- ");
            aliceWantsTwoBeers ();
            dormir();
            removeBeersFromStock ();
            System.out.println ("####################################  n");
            printDatabaseState ();
            dormir();
            System.out.println (" n ######### AVEC TRANSACTION #########");
            System.out.println ("Alice veut 2 bières supplémentaires.");
            System.out.println ("Maintenant, nous pouvons mettre à jour les 2 collections simultanément.");
            System.out.println ("Les 2 opérations ne se produisent que lorsque la transaction est validée");
            System.out.println ("------------------------------------------- -------------------------------- ");
            aliceWantsTwoExtraBeersInTransactionThenCommitOrRollback ();
            dormir();
            System.out.println (" n ######### AVEC TRANSACTION #########");
            System.out.println ("Alice veut 2 bières supplémentaires.");
            System.out.println ("Cette fois, nous n'avons pas assez de bières en stock pour que la transaction soit annulée.");
            System.out.println ("------------------------------------------- -------------------------------- ");
            aliceWantsTwoExtraBeersInTransactionThenCommitOrRollback ();
            client.close ();
        }
    
        aliceWantsTwoExtraBeersInTransactionThenCommitOrRollback () {void privé () {
            ClientSession session = client.startSession ();
            essayez {
                session.startTransaction (TransactionOptions.builder (). writeConcern (WriteConcern.MAJORITY) .build ());
                aliceWantsTwoExtraBeers (session);
                dormir();
                removeBeerFromStock (session);
                session.commitTransaction ();
            } catch (MongoCommandException e) {
                session.abortTransaction ();
                System.out.println ("####### ROLLBACK TRANSACTION #######");
            } enfin {
                session.close ();
                System.out.println ("####################################  n");
                printDatabaseState ();
            }
        }
    
        annulation privée removeBeersFromStock () {
            System.out.println ("Essayer de mettre à jour le stock de bière: -2 bières.");
            essayez {
                productCollection.updateOne (filterId, stockUpdate);
            } catch (MongoCommandException e) {
                System.out.println ("##### MongoCommandException #####");
                System.out.println ("##### STOCK NE PEUT PAS ÊTRE NÉGATIF ​​#####");
                lancer e;
            }
        }
    
        void privé removeBeerFromStock (session ClientSession) {
            System.out.println ("Essayer de mettre à jour le stock de bière: -2 bières.");
            essayez {
                productCollection.updateOne (session, filterId, stockUpdate);
            } catch (MongoCommandException e) {
                System.out.println ("##### MongoCommandException #####");
                System.out.println ("##### STOCK NE PEUT PAS ÊTRE NÉGATIF ​​#####");
                lancer e;
            }
        }
    
        annulation privée aliceWantsTwoBeers () {
            System.out.println ("Alice ajoute 2 bières dans son panier.");
            cartCollection.insertOne (nouveau Cart ("Alice", Collections.singletonList (new Cart.Item (BEER_ID, 2, BEER_PRICE))));
        }
    
        vide privé aliceWantsTwoExtraBeers (session ClientSession) {
            System.out.println ("Mise à jour du panier Alice: ajout de 2 bières.");
            cartCollection.updateOne (session et (filterAlice, matchBeer), incrementBeers);
        }
    
        privé void insertProductBeer () {
            productCollection.insertOne (nouveau produit (BEER_ID, 5, BEER_PRICE));
        }
    
        clearCollections void privées () {
            productCollection.deleteMany (new BsonDocument ());
            cartCollection.deleteMany (new BsonDocument ());
        }
    
        void privé printDatabaseState () {
            System.out.println ("Etat de la base de données:");
            printProducts (productCollection.find (). into (new ArrayList <> ()));
            printCarts (cartCollection.find (). into (nouveau ArrayList <> ()));
            System.out.println ();
        }
    
        Produits d'impression privés (Liste  produits) {
            products.forEach (System.out :: println);
        }
    
        printCarts void privé (Liste  carts) {
            si (carts.isEmpty ())
                System.out.println ("No carts ...");
            autre
                carts.forEach (System.out :: println);
        }
    
        sommeil vide privé () {
            System.out.println ("Sleeping 3 seconds ...");
            essayez {
                Thread.sleep (3000);
            } catch (InterruptedException e) {
                System.err.println ("Oups ...");
                e.printStackTrace ();
            }
        }
    }
    

    Voici la console du Change Stream:

     $ ./change-streams.sh
    
    Regarder test.cart
    Regarder le test.produit
    
    Horodatage {valeur = 6570052721557110786, secondes = 1529709604, inc = 2} => Panier {id = 'Alice', articles = [Item{productId=beer, quantity=2, price=3}]}
    
    Horodatage {valeur = 6570052734442012673, secondes = 1529709607, inc = 1} => Produit {id = 'beer', stock = 3, prix = 3}
    
    Horodatage {valeur = 6570052764506783745, secondes = 1529709614, inc = 1} => Produit {id = 'beer', stock = 1, prix = 3}
    Horodatage {value = 6570052764506783745, seconds = 1529709614, inc = 1} => Cart {id = 'Alice', articles = [Item{productId=beer, quantity=4, price=3}]}
    

    Comme vous pouvez le voir ici, nous n'obtenons que quatre opérations car les deux dernières opérations n'ont jamais été validées dans la base de données et le flux de modifications n'a donc rien à montrer.

    Vous pouvez également noter que les deux premières différent parce que nous n'avons pas utilisé de transaction pour les deux premières opérations, et que les deux dernières opérations partagent la même heure de cluster car nous avons utilisé le nouveau système de transaction multi-documents MongoDB 4.0, et sont donc atomiques.

    du processus Java Transaction qui résume tout ce que j'ai dit précédemment.

     $ ./transactions.sh
    Etat de la base de données:
    Produit {id = 'beer', stock = 5, prix = 3}
    Pas de chariots ...
    
    ######### AUCUNE TRANSACTION #########
    Alice veut 2 bières.
    Nous devons créer un panier dans la collection «panier» et mettre à jour le stock dans la collection «produit».
    Les 2 actions sont corrélées mais ne peuvent pas être exécutées sur la même heure de cluster.
    Toute erreur bloquant une opération peut entraîner une erreur de stock ou une vente de bière que nous ne pouvons pas réaliser car nous n'avons pas de stock.
    -------------------------------------------------- -------------------------
    Alice ajoute 2 bières dans son panier.
    Dormir 3 secondes ...
    Essayer de mettre à jour le stock de bière: -2 bières.
    ####################################
    
    Etat de la base de données:
    Produit {id = 'beer', stock = 3, prix = 3}
    Cart {id = 'Alice', articles = [Item{productId=beer, quantity=2, price=3}]}
    
    Dormir 3 secondes ...
    
    ######### AVEC TRANSACTION #########
    Alice veut 2 bières supplémentaires.
    Maintenant, nous pouvons mettre à jour les 2 collections simultanément.
    Les 2 opérations ne se produisent que lorsque la transaction est validée.
    -------------------------------------------------- -------------------------
    Mise à jour du panier Alice: ajout de 2 bières.
    Dormir 3 secondes ...
    Essayer de mettre à jour le stock de bière: -2 bières.
    ####################################
    
    Etat de la base de données:
    Produit {id = 'beer', stock = 1, prix = 3}
    Cart {id = 'Alice', items = [Item{productId=beer, quantity=4, price=3}]}
    
    Dormir 3 secondes ...
    
    ######### AVEC TRANSACTION #########
    Alice veut 2 bières supplémentaires.
    Cette fois, nous n’avons pas assez de bières en stock pour que la transaction soit annulée.
    -------------------------------------------------- -------------------------
    Mise à jour du panier Alice: ajout de 2 bières.
    Dormir 3 secondes ...
    Essayer de mettre à jour le stock de bière: -2 bières.
    ##### MongoCommandException #####
    ##### STOCK NE PEUT PAS ÊTRE NÉGATIF ​​#####
    ####### ROLLBACK TRANSACTION #######
    ####################################
    
    Etat de la base de données:
    Produit {id = 'beer', stock = 1, prix = 3}
    Cart {id = 'Alice', items = [Item{productId=beer, quantity=4, price=3}]}
    

    Prochaines étapes

    Merci d'avoir pris le temps de lire mon message – J'espère que vous l'avez trouvé utile et intéressant.
    Pour rappel, tout le code est disponible sur ce dépôt Github pour

    Si vous cherchez un moyen très simple de démarrer avec MongoDB, vous pouvez le faire en seulement 5 clics sur notre service de base de données Atlas de MongoDB dans le nuage.

    , les transactions ACID multi-documents ne sont pas la seule nouveauté de MongoDB 4.0, alors n'hésitez pas à consulter notre cours gratuit sur MongoDB University M040: nouvelles fonctionnalités et outils dans MongoDB 4.0 et nos guide des nouveautés de MongoDB 4.0 où vous pouvez en apprendre davantage sur les conversions de type natif, les nouveaux outils de visualisation et d'analyse et l'intégration de Kubernetes.




    Source link