Fermer

septembre 28, 2023

Comment intégrer MarkLogic avec KendoReact Grid et Map

Comment intégrer MarkLogic avec KendoReact Grid et Map


Utilisez les composants KendoReact Grid et Map pour créer une interface personnalisable et accessible alimentée par la puissante base de données MarkLogic.

Progrès MarkLogique est une base de données conçue dès le départ pour rendre des quantités massives de données hétérogènes facilement accessibles via la recherche.

La philosophie de conception derrière l’évolution de MarkLogic est que le stockage des données ne constitue qu’une partie de la solution. Les données doivent également être récupérées et présentées rapidement et facilement d’une manière qui ait du sens pour différents types d’utilisateurs. De plus, les données doivent être conservées de manière fiable par une solution logicielle évolutive de niveau entreprise qui fonctionne sur du matériel standard.

Dans cet article de blog, nous allons créer un composant Grid avec des opérations CRUD et un KendoRéagir Carte qui utilise MarkLogic pour visualiser les données.

Gardez à l’esprit que tout au long de ce billet de blog, j’utiliserai Progress KendoReact Grille de données et KendoReact Carte Composants. Pour savoir comment être opérationnel avec ces composants, je vous recommande fortement de consulter le Premiers pas avec KendoReact article, qui vous expliquera comment utiliser la grille de données React.

Configurer MarkLogic

Au cas où vous seriez bloqué, le code complet de ce projet se trouve dans ce Dépôt GitHub.

Nous devons d’abord installer MarkLogic. Visitez leur documentation officielle et suivez les étapes nécessaires à votre système d’exploitation.

Après avoir installé MarkLogic, visitez localhost:8001 dans votre navigateur Web pour configurer votre serveur MarkLogic avec un nom d’utilisateur et un mot de passe administrateur. (Dans notre exemple, nous utilisons admin/admin pour le nom d’utilisateur/mot de passe.) Au cours de ce processus, vous pouvez ignorer la participation à un cluster et saisir n’importe quoi pour le mot de passe du portefeuille.

Créez un nouveau répertoire de projet nommé kendo-react-marklogic-example. Dans ce répertoire, créez un répertoire nommé setup. Ouvrez un terminal dans le répertoire d’installation et exécutez :

npm init

afin de créer un nouveau package.json puis d’exécuter :

npm install  --save  request request-promise colors press-any-key

Dans le même répertoire créez un fichier config.js qui contiendra nos paramètres de configuration de MarkLogic :

const config = {};
 
config.project = {
  name: "kendo-react-marklogic-sample"
};
 
config.auth = {
  user: "admin",
  pass: "admin",
  sendImmediately: false
};
 
config.host = "localhost";
 
config.databases = {
  content: {
    name: config.project.name + "-content"
  },
  modules: {
    name: config.project.name + "-modules"
  },
};
 
config.rest = {
  "rest-api": {
    name: config.project.name + "-rest",
    database: config.databases.content.name,
    "modules-database": config.databases.modules.name,
    port: 8077,
    "error-format": "json"
  },
  security: {
    authentication: "basic"
  },
  options: {
    name: "search-options",
    file: "search-options.xml"
  }
};
 
config.user = {
  "user-name": config.project.name + "-user",
  "password": "password",
  "role": [ "rest-admin", "rest-writer" ]
}
 
config.content = [
  {
    collection: "product",
    path: "/data/products"
  }
];
 
config.modules = {
    path: "/data/modules"
};
 
config.pause = 10000;
 
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  module.exports = config;
}

Ici, nous configurons notre base de données et notre authentification. Dans le même répertoire, créez un fichier appelé setup.js qui contiendra notre configuration MarkLogic et ajoutez le code suivant :

const config = require(process.argv[2] || './config'),
      client = require('./client.js');
 
const setup = async (config) => {
  console.log(
    '                            SETUP STARTED                             '.gray.bold.inverse
  );
  try {
    await client.getConfig(config);
  } catch(error) {
    console.error("Cannot connect to MarkLogic, ending setup.".red);
    process.exit();
  }
  await Promise.all([
    client.createDatabase(config.databases.content.name, config),
    client.createDatabase(config.databases.modules.name, config)
  ]);
  await Promise.all([
    client.createForest(config.databases.content.name, config),
    client.createForest(config.databases.modules.name, config),
  ]);
  await client.createREST(config);
  await client.setRESTAuth(config);
  await client.createUser(config);
  console.log('Loading data...'.green);
  await Promise.all([
    client.loadContent(config),
    client.loadSearchOptions(config)
  ]);
  console.log(
      '                            SETUP FINISHED                            '.gray.bold.inverse
  );
}
 
setup(config);

Démarrez le service MarkLogic sur votre ordinateur en suivant les étapes correspondant à votre système d’exploitation :

https://docs.marklogic.com/11.0/guide/installation-guide/en/procedures/starting-marklogic-server.html

Voici à quoi ressemble le démarrage de MarkLogic sous Windows :

Ouvrez un terminal et dans le répertoire d’installation, exécutez node setup.js et ouvrez localhost:8077. Lors de sa première exécution, vous serez invité à créer un nom d’utilisateur et un mot de passe qui doivent être ajoutés à la configuration :

config.auth = {
  user: "admin",
  pass: "admin",
  sendImmediately: false
};

Configuration du projet pour notre serveur Express

Maintenant que MarkLogic est opérationnel, nous allons voir comment configurer un serveur qui servira nos points de terminaison. Nous utiliserons Express.

Dans le répertoire du projet, nous allons créer un répertoire nommé server. À l’intérieur, nous exécuterons npm init afin de créer un fichier package.json. Une fois que vous avez terminé, exécutez les commandes suivantes pour installer express et cors :

npm install express –save
npm install cors –save
npm install @progress/kendo-data-query –save

Ensuite, créez un fichier index.js dans le même répertoire qui sera notre serveur et ajoutez les lignes suivantes :

const express = require("express");
const app = express();
const cors = require("cors");
app.use(cors());
app.use(express.json());
 
app.listen(4000, () => {
    console.log("Server is running on port 4000");
});

Ici, nous exigeons express et cors et configurons notre application pour qu’elle écoute le port 4000.

L’étape suivante consiste à importer nos produits.js et notre processus à partir du package @progress/kendo-data-query afin que nous puissions appliquer des descripteurs d’opération aux données.

Voici à quoi ressemble notre code mis à jour :

const express = require("express");
const app = express();
const cors = require("cors");
app.use(cors());
app.use(express.json());
const data = require('./products')
const { process } = require('@progress/kendo-data-query');
 
app.listen(4000, () => {
    console.log("Server is running on port 4000");
});

Nous sommes maintenant prêts à créer nos points de terminaison qui seront ensuite consommés côté client.

app.get("/products", (req, res) => {
    let dataState = req.query.dataState;
    
    let skip = parseInt(dataState.skip);
    let take = parseInt(dataState.take);
    res.send(process(parsedData,
        {
            skip: skip,
            take: take,
            group: dataState.group,
            sort: dataState.sort,
            filter: dataState.filter
        }
    ));
});
 
app.put("/update", (req, res) => {
    let dataState = req.body.dataState;
    const item = req.body.item;
    const id = item.ProductID;
    
    let index = parsedData.findIndex(item => item.ProductID === id);
    parsedData[index] = item;
    parsedData[index].inEdit = false;
    
    res.send(process(parsedData, dataState));
});
 
app.post("/create", (req, res) => {
    const item = req.body.item
    let dataState = req.body.dataState;
    const id = parsedData.length + 1;
    item.ProductID = id;
    item.inEdit = false;
    parsedData.unshift(item);
    res.send(process(parsedData, dataState));
});
 
app.delete("/delete/:id", (req, res) => {
    let dataState = req.body.dataState;
    const id = parseInt(req.params.id);
    
    let index = parsedData.findIndex(item => item.ProductID === id);
    parsedData.splice(index, 1);
    
    res.send(process(parsedData, dataState));
});

Cela nous permettra de créer, mettre à jour et supprimer un élément de notre grille plus tard.

Voici à quoi ressemble notre fichier index.js avec toutes les importations et tous les points de terminaison configurés :

const express = require("express");
const app = express();
const cors = require("cors");
 
const data = require('./products')
const mapData = require('../setup/data/countries-users.json')
 
const { process } = require('@progress/kendo-data-query');
 
const currentYear = new Date().getFullYear();
 
const parsedData = data.sampleProducts.map(product => {
    const date = new Date(product.FirstOrderedOn);
    date.setFullYear(currentYear);
    product.FirstOrderedOn = date.toISOString()
    return product;
})
 
app.use(cors());
app.use(express.json());
 
app.get("/products", (req, res) => {
    let dataState = req.query.dataState;
    
    let skip = parseInt(dataState.skip);
    let take = parseInt(dataState.take);
    res.send(process(parsedData,
        {
            skip: skip,
            take: take,
            group: dataState.group,
            sort: dataState.sort,
            filter: dataState.filter
        }
    ));
});
 
app.get("/map", (req, res) => {
    res.send(mapData)
});
 
app.put("/update", (req, res) => {
    let dataState = req.body.dataState;
    const item = req.body.item;
    const id = item.ProductID;
    
    let index = parsedData.findIndex(item => item.ProductID === id);
    parsedData[index] = item;
    parsedData[index].inEdit = false;
    
    res.send(process(parsedData, dataState));
});
 
app.post("/create", (req, res) => {
    const item = req.body.item
    let dataState = req.body.dataState;
    const id = parsedData.length + 1;
    item.ProductID = id;
    item.inEdit = false;
    parsedData.unshift(item);
    res.send(process(parsedData, dataState));
});
 
app.delete("/delete/:id", (req, res) => {
    let dataState = req.body.dataState;
    const id = parseInt(req.params.id);
    
    let index = parsedData.findIndex(item => item.ProductID === id);
    parsedData.splice(index, 1);
    
    res.send(process(parsedData, dataState));
});
 
 
app.listen(4000, () => {
    console.log("Server is running on port 4000");
});

Démarrez le serveur en exécutant la commande suivante depuis le répertoire du serveur :

node index.js

Cela démarrera le serveur et vous devriez pouvoir voir le message Le serveur s’exécute sur le port 4000 dans le terminal.

Nous sommes maintenant prêts à créer notre configuration MarkLogic.

Création de notre interface utilisateur de grille et de carte KendoReact

Sortez du répertoire du serveur avec cd../ et créez un nouveau répertoire appelé ui. Dans le nouveau répertoire, créez une nouvelle application React en exécutant :

npx create-react-app my-app --use-npm

Nous devrons installer Axios, qui s’occupera de nos requêtes, et de nos dépendances KendoReact :

npm install --save axios @progress/kendo-react-grid @progress/kendo-data-query @progress/kendo-react-data-tools @progress/kendo-react-inputs @progress/kendo-react-intl @progress/kendo-react-dropdowns @progress/kendo-react-dateinputs @progress/kendo-drawing @progress/kendo-react-animation @progress/kendo-licensing @progress/kendo-react-buttons @progress/kendo-react-treeview @progress/kendo-react-popup @progress/kendo-svg-icons @progress/kendo-react-map @progress/kendo-drawing @progress/kendo-react-treelist @progress/kendo-theme-default

Accédez au répertoire src à l’intérieur my-app et créez un nouveau répertoire appelé context. Ici, nous stockerons notre contexte qui sera réutilisé via l’application. Le contexte fournit un moyen de transmettre des données à travers l’arborescence des composants sans avoir à transmettre manuellement les accessoires à chaque niveau. Dans le dossier, créez un fichier appelé data-context.js et définir le contexte requis pour nos opérations CRUD :

import { createContext } from 'react';
 
const DataContext = createContext({
    enterEdit: () => {},
    remove: () => {},
    add: () => {},
    discard: () => {},
    update: () => {},
    cancel: () => {}
});
export default DataContext;

Ensuite, nous créerons nos composants. Dans le même répertoire src, créez-en un nouveau appelé composants. À l’intérieur, créez un nouveau fichier CommandCell.jsx. Ce composant contiendra les boutons pour les boutons Supprimer, Ajouter, Supprimer et Mettre à jour :

import * as React from "react";
import DataContext from '../context/data-context';
import { Button } from "@progress/kendo-react-buttons";
 
const MyCommandCell = props => {
    const currentContext = React.useContext(DataContext);
    const { dataItem } = props;
    const isNewItem = dataItem.ProductID === undefined;
 
    const inEdit = dataItem.inEdit;
 
    const handleAddUpdate = React.useCallback(() =>{
        if(isNewItem){
            currentContext.add(dataItem)
        } else {
            currentContext.update(dataItem)
        }
    },[currentContext, dataItem, isNewItem])
 
    const handleDiscardCancel = React.useCallback(()=>{
        isNewItem ? currentContext.discard(dataItem) : currentContext.cancel()
    },[currentContext, dataItem, isNewItem])
 
    const handleEdit = React.useCallback(()=> {
        currentContext.enterEdit(dataItem)
    },[currentContext, dataItem])
 
    const handleDelete = React.useCallback(()=> {
        window.confirm("Confirm deleting: " + dataItem.ProductName) && currentContext.remove(dataItem)
    },[currentContext, dataItem])
 
    if(props.rowType === 'groupHeader') return null;
 
    return inEdit ?
            (<td className="k-command-cell">
                <Button onClick={handleAddUpdate}>
                    {isNewItem ? "Add" : "Update"}
                </Button>
                <Button onClick={handleDiscardCancel}>
                    {isNewItem ? "Discard" : "Cancel"}
                </Button>
            </td>) :
            (<td className="k-command-cell">
                <Button themeColor="primary" onClick={handleEdit}>
                    Edit
                </Button>
                <Button onClick={handleDelete}>
                    Remove
                </Button>
            </td>);
};
 
export default MyCommandCell;

Dans le répertoire des composants, créez un fichier appelé DropDownCell.jsx. Ce sera notre composant déroulant qui contiendra le code suivant :

import * as React from 'react';
import {
  DropDownList,
} from '@progress/kendo-react-dropdowns';
 
const categoryData = [
    {
      CategoryID: 1,
      CategoryName: 'Beverages',
      Description: 'Soft drinks, coffees, teas, beers, and ales',
    },
    {
      CategoryID: 2,
      CategoryName: 'Condiments',
      Description: 'Sweet and savory sauces, relishes, spreads, and seasonings',
    },
    {
      CategoryID: 6,
      CategoryName: 'Meat/Poultry',
      Description: 'Prepared meats',
    },
    {
      CategoryID: 7,
      CategoryName: 'Produce',
      Description: 'Dried fruit and bean curd',
    },
    {
      CategoryID: 8,
      CategoryName: 'Seafood',
      Description: 'Seaweed and fish',
    },
  ];
 
const DropDownCell = (props) => {
 
  if(props.rowType === 'groupHeader') return null;
 
 
  let fieldComplex = props.field.split('.');
 
  const handleChange = (e) => {
    if (props.onChange) {
      props.onChange({
        dataIndex: 0,
        dataItem: props.dataItem,
        field: fieldComplex[0],
        syntheticEvent: e.syntheticEvent,
        value: e.value,
      });
    }
  };
 
  const { dataItem } = props;
  const dataValue =
  dataItem[fieldComplex[0]] === null || dataItem[fieldComplex[0]][fieldComplex[1]] === null
      ? ''
      : dataItem[fieldComplex[0]][fieldComplex[1]];
 
  return (
    <td>
      {dataItem.inEdit ? (
        <DropDownList
          style={{ width: '100%' }}
          onChange={handleChange}
          value={dataItem[fieldComplex[0]]}
          data={categoryData}
          textField={fieldComplex[1]}
          defaultItem={{CategoryID: 0, CategoryName: 'Choose Category'}}
        />
      ) : (
        dataValue.toString()
      )}
    </td>
  );
};
 
export default DropDownCell;

Une fois ces deux composants configurés, il est temps de créer notre grille. Ouvrez App.js et importez notre CSS, quelques hooks React, la grille KendoReact, Axios, nos cellules personnalisées et notre contexte. Ajoutez ce qui suit en haut d’App.js :

import '@progress/kendo-theme-default/dist/all.css'
import { useState, useEffect } from "react";
import { Grid, GridColumn, GridToolbar } from '@progress/kendo-react-grid';
import { mapTree } from "@progress/kendo-react-treelist";
import { clone } from '@progress/kendo-react-common';
import { Button } from "@progress/kendo-react-buttons";
import MyCommandCell from './components/CommandCell';
import Axios from "axios";
import DropDownCell from './components/DropDownCell';
import DataContext from './context/data-context';

Ensuite, nous nous occuperons de l’état de notre application en créant des hooks. Ajoutez ce qui suit à l’intérieur du App() fonction:

  const [data, setData] = useState([]);
  const [itemBeforeEdit, setItemBeforeEdit] = useState({})
  const [dataState, setDataState] = useState({ take: 8, skip: 0, group: [{field: 'ProductName'}] })
  const [total, setTotal] = useState(0);

Une fois cela fait, nous utiliserons le hook Effect pour récupérer nos données à partir des points de terminaison que nous avons créés précédemment. Ajoutez ce qui suit après les hooks d’état :

useEffect(() => {
    Axios.get("http://localhost:4000/products", {
      params: {
        dataState: dataState
      }
    }).then((response) => {
      let parsedDataNew = mapTree(response.data.data, 'items', (product) => {
        product.FirstOrderedOn = product.FirstOrderedOn !== null ? new Date(product.FirstOrderedOn) : null;
        return product
      })
      setTotal(response.data.total)
      setData([...parsedDataNew]);
    });
  }, [dataState])
 
  const addRecord = () => {
    let newRecord = { ProductID: undefined, FirstOrderedOn: new Date(), Category: null, inEdit: true, Discontinued: false, UnitsInStock: 1, ProductName: null }
    let newData = [...data];
    newData.unshift(newRecord);
    setData(newData)
  }
 
  const handleItemChange = (event) => {
    let newData = mapTree(data, 'items', item => {
      if (event.dataItem.ProductID === item.ProductID) {
        item[event.field] = event.value;
      }
      return item;
    })
    setData(newData);
  }
 
  const enterEdit = (dataItem) => {
    let newData = mapTree(data, "items", (item) => {
      dataItem.ProductID === item.ProductID ? item.inEdit = true : item.inEdit = false;
      return item;
    });
 
    setItemBeforeEdit(clone(dataItem));
    setData(newData);
  }
 
  const remove = (dataItem) => {
    Axios.delete(`http://localhost:4000/delete/${dataItem.ProductID}`, { data : {dataState: dataState}}).then(
      (response) => {
        let newData = mapTree(response.data.data,'items', item => {
          item.FirstOrderedOn = new Date(item.FirstOrderedOn);
          return item;
        })
        setData([...newData]);
        setTotal(response.data.total)
      }
    );
  }
 
  const add = (dataItem) => {
    Axios.post("http://localhost:4000/create", { item: dataItem, dataState: dataState }).then((response) => {
      let newData = mapTree(response.data.data,'items', item => {
        item.FirstOrderedOn = new Date(item.FirstOrderedOn);
        return item;
      })
      setData(newData);
      setTotal(response.data.total)
    });
  }
 
  const discard = () => {
    let hasGroup = dataState.group.length > 0 ? true : false
    let newData = []
    hasGroup ? newData = data.filter(item => item.value !== undefined) : newData = data.filter(item => item.ProductID !== undefined )
    setData(newData);
  }
 
  const update = (dataItem) => {
    Axios.put("http://localhost:4000/update", { item: dataItem, dataState: dataState }).then(
      (response) => {
        let newData = mapTree(response.data.data,'items', item => {
          item.FirstOrderedOn = new Date(item.FirstOrderedOn);
          return item;
        })
        setData(newData);
      }
    );
  }
 
  const cancel = () => {
    let newData = mapTree(data, 'items', item => {
      if (item.ProductID === itemBeforeEdit.ProductID) {
        item = itemBeforeEdit;
        item.inEdit = false;
      }
      return item;
    })
    setData(newData);
  }
 
    const handleDataStateChange = (event) => {
      setDataState(event.dataState)
  }
 

Notre prochaine étape consiste à restituer le composant KendoReact Grid dans notre DataContext.Provider. Remplacez le contenu renvoyé par ce qui suit :

<div className=”App”>    
<DataContext.Provider
        value={{
          enterEdit: enterEdit,
          remove: remove,
          add: add,
          discard: discard,
          update: update,
          cancel: cancel
        }}
      >
        <Grid
          style={{
            height: "520px",
          }}
          data={data}
          editField="inEdit"
          onItemChange={handleItemChange}
          onDataStateChange={handleDataStateChange}
          {...dataState}
          pageable
          sortable
          filterable
          groupable
          total={total}
        >
          <GridToolbar>
            <div>
              <Button
                title="Add new"
                className="k-button k-button-md k-rounded-md k-button-solid k-button-solid-primary"                onClick={addRecord}
              >
                Add new record
              </Button>
 
            </div>
          </GridToolbar>
          <GridColumn field="ProductID" title="Id"  editable={false} filterable={false} />
          <GridColumn field="ProductName" title="Name" />
          <GridColumn field="Category.CategoryName" title="Category" cell={DropDownCell} />
          <GridColumn field="FirstOrderedOn" title="First Ordered On" editor="date" filter="date" format={'{0:d}'} />
          <GridColumn
            field="UnitsInStock"
            title="Units"
            editor="numeric"
            filter="numeric"
          />
          <GridColumn field="Discontinued" title="Discontinued" editor="boolean" filter="boolean" />
          <GridColumn cell={MyCommandCell}  filterable={false} />
        </Grid>
         </DataContext.Provider>
</div>

Nous transmettons les produits que nous avons récupérés plus tôt dans notre hook Effect à notre grille KendoReact via le data accessoires. Les composants que nous avons créés précédemment sont transmis sous forme de cellules personnalisées à la catégorie et aux dernières colonnes de la grille et aux fonctions que nous avons définies dans notre data-context.js fichier sont transmis sous forme de valeurs à notre fournisseur. Courir npm start et ouvert localhost:3000 et maintenant vous devriez voir notre grille KendoReact rendue qui peut ajouter un nouvel enregistrement, en modifier un existant ou le supprimer et annuler l’édition.

L'utilisateur navigue dans la grille, trie par nom, recherche, modifie, supprime

Il ne reste plus qu’à ajouter notre carte KendoReact. Naviguez dans le dossier du serveur et ouvrez index.js. Importez le fichier country-users.json qui se trouve dans le dossier data de la structure du projet :

const mapData = require('../setup/data/countries-users.json')

Ensuite, nous devons configurer notre point de terminaison pour les données qui seront utilisées par la carte. Cela peut être défini ci-dessous nos autres requêtes dans index.js :

app.get("/map", (req, res) => {
    res.send(mapData)
});

Appuyez sur Ctrl + C (ou Cmd + C) dans votre terminal pour arrêter le serveur, puis exécutez à nouveau node index.js pour le démarrer. Dans App.js, importez la Map, MapLayers, MapShapeLayer :

import { Map, MapLayers, MapShapeLayer } from '@progress/kendo-react-map';

Nous devrons gérer l’état de la carte de la même manière que nous l’avons fait pour notre grille. Ajoutez ceci en haut du App() fonction:

const [mapData, setMapData] = useState({})

Nous allons créer une fonction getChartData à l’intérieur App() qui récupérera les données de notre point de terminaison de carte et enregistrera la réponse dans mapData :

const getChartData = () => {
  Axios.get("http://localhost:4000/map", {
    }).then((response) => {
      setMapData(response.data.features)
    });
};

Les données seront récupérées en appelant le getChartData() fonction à l’intérieur de notre crochet d’effet :

  useEffect(() => {
    getChartData()
    Axios.get("http://localhost:4000/products", {
      params: {
        dataState: dataState
      }
    }).then((response) => {
      let parsedDataNew = mapTree(response.data.data, 'items', (product) => {
        product.FirstOrderedOn = product.FirstOrderedOn !== null ? new Date(product.FirstOrderedOn) : null;
        return product
      })
      setTotal(response.data.total)
      setData([...parsedDataNew]);
    });
  }, [dataState])

Notre prochaine étape consiste à créer un composant MapContainer pour notre carte, ainsi qu’à définir le style de forme et nos points centraux :

const shapeStyle = {
    fill: {
      color: 'green'
    }
  };
  const center = [40, 0];
const MapContainer = () => {
  return <div> 
     <Map center={center}>
      <MapLayers>
        <MapShapeLayer data={mapData} style={shapeStyle} />
      </MapLayers>
    </Map>
  </div>
}

Tout ce que nous devons faire maintenant pour rendre notre carte est d’ajouter notre MapContainer dans le balisage sous la grille :

<div className="map-container">
         <MapContainer/>
         </div>

Ouvrez localhost et vous verrez maintenant le composant Map rendu avec succès.

Conclusion

KendoReact peut fournir une belle interface utilisateur et peut facilement être intégré à MarkLogic, qui prend en charge plusieurs cas d’utilisation tout en offrant une sécurité, une gouvernance et une cohérence unifiées des données.




Source link

septembre 28, 2023