1. Présentation
Dans ce didacticiel, nous allons apprendre à lire des données JSON à partir de fichiers et à les importer dans MongoDB à l'aide de Spring Boot. Cela peut être utile pour de nombreuses raisons :restauration de données, insertion groupée de nouvelles données ou insertion de valeurs par défaut. MongoDB utilise JSON en interne pour structurer ses documents, donc naturellement, c'est ce que nous utiliserons pour stocker les fichiers importables. En texte brut, cette stratégie a également l'avantage d'être facilement compressible.
De plus, nous apprendrons comment valider nos fichiers d'entrée par rapport à nos types personnalisés si nécessaire. Enfin, nous exposerons une API afin que nous puissions l'utiliser pendant l'exécution dans notre application Web.
2. Dépendances
Ajoutons ces dépendances Spring Boot à notre pom.xml :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
Nous aurons également besoin d'une instance en cours d'exécution de MongoDB, qui nécessite une application.properties correctement configurée fichier.
3. Importation de chaînes JSON
La façon la plus simple d'importer JSON dans MongoDB est de le convertir en un "org.bson.Document ” objet en premier. Cette classe représente un document MongoDB générique sans type spécifique. Par conséquent, nous n'avons pas à nous soucier de créer des référentiels pour tous les types d'objets que nous pourrions importer.
Notre stratégie prend JSON (à partir d'un fichier, d'une ressource ou d'une chaîne), le convertit en Document s, et les enregistre en utilisant MongoTemplate . Les opérations par lots fonctionnent généralement mieux car le nombre d'allers-retours est réduit par rapport à l'insertion de chaque objet individuellement.
Plus important encore, nous considérerons que notre entrée n'a qu'un seul objet JSON par saut de ligne. De cette façon, nous pouvons facilement délimiter nos objets. Nous encapsulerons ces fonctionnalités dans deux classes que nous créerons :ImportUtils et ImportJsonService . Commençons par notre classe de service :
@Service
public class ImportJsonService {
@Autowired
private MongoTemplate mongo;
}
Ensuite, ajoutons une méthode qui analyse les lignes de JSON dans les documents :
private List<Document> generateMongoDocs(List<String> lines) {
List<Document> docs = new ArrayList<>();
for (String json : lines) {
docs.add(Document.parse(json));
}
return docs;
}
Ensuite, nous ajoutons une méthode qui insère une liste de Document objets dans la collection souhaitée . De plus, il est possible que l'opération par lots échoue partiellement. Dans ce cas, nous pouvons retourner le nombre de documents insérés en vérifiant la cause de l'exception :
private int insertInto(String collection, List<Document> mongoDocs) {
try {
Collection<Document> inserts = mongo.insert(mongoDocs, collection);
return inserts.size();
} catch (DataIntegrityViolationException e) {
if (e.getCause() instanceof MongoBulkWriteException) {
return ((MongoBulkWriteException) e.getCause())
.getWriteResult()
.getInsertedCount();
}
return 0;
}
}
Enfin, combinons ces méthodes. Celui-ci prend l'entrée et renvoie une chaîne indiquant le nombre de lignes lues et insérées avec succès :
public String importTo(String collection, List<String> jsonLines) {
List<Document> mongoDocs = generateMongoDocs(jsonLines);
int inserts = insertInto(collection, mongoDocs);
return inserts + "/" + jsonLines.size();
}
4. Cas d'utilisation
Maintenant que nous sommes prêts à traiter les entrées, nous pouvons créer des cas d'utilisation. Créons le ImportUtils classe pour nous aider avec cela. Cette classe sera responsable de la conversion des entrées en lignes JSON. Il ne contiendra que des méthodes statiques. Commençons par celui pour lire une simple String :
public static List<String> lines(String json) {
String[] split = json.split("[\\r\\n]+");
return Arrays.asList(split);
}
Puisque nous utilisons des sauts de ligne comme délimiteurs, regex fonctionne très bien pour diviser les chaînes en plusieurs lignes. Cette expression régulière gère à la fois les fins de ligne Unix et Windows. Ensuite, une méthode pour convertir un fichier en une liste de chaînes :
public static List<String> lines(File file) {
return Files.readAllLines(file.toPath());
}
De même, nous terminons avec une méthode pour convertir une ressource classpath en liste :
public static List<String> linesFromResource(String resource) {
Resource input = new ClassPathResource(resource);
Path path = input.getFile().toPath();
return Files.readAllLines(path);
}
4.1. Importer un fichier au démarrage avec une CLI
Dans notre premier cas d'utilisation, nous allons implémenter une fonctionnalité pour importer un fichier via des arguments d'application. Nous allons profiter de Spring Boot ApplicationRunner interface pour le faire au démarrage. Par exemple, nous pouvons lire les paramètres de la ligne de commande pour définir le fichier à importer :
@SpringBootApplication
public class SpringBootJsonConvertFileApplication implements ApplicationRunner {
private static final String RESOURCE_PREFIX = "classpath:";
@Autowired
private ImportJsonService importService;
public static void main(String ... args) {
SpringApplication.run(SpringBootPersistenceApplication.class, args);
}
@Override
public void run(ApplicationArguments args) {
if (args.containsOption("import")) {
String collection = args.getOptionValues("collection")
.get(0);
List<String> sources = args.getOptionValues("import");
for (String source : sources) {
List<String> jsonLines = new ArrayList<>();
if (source.startsWith(RESOURCE_PREFIX)) {
String resource = source.substring(RESOURCE_PREFIX.length());
jsonLines = ImportUtils.linesFromResource(resource);
} else {
jsonLines = ImportUtils.lines(new File(source));
}
String result = importService.importTo(collection, jsonLines);
log.info(source + " - result: " + result);
}
}
}
}
Utilisation de getOptionValues() nous pouvons traiter un ou plusieurs dossiers. Ces fichiers peuvent provenir de notre chemin de classe ou de notre système de fichiers. Nous les différencions en utilisant le RESOURCE_PREFIX . Chaque argument commençant par "classpath : ” sera lu à partir de notre dossier de ressources au lieu du système de fichiers. Après cela, ils seront tous importés dans la collection souhaitée .
Commençons à utiliser notre application en créant un fichier sous src/main/resources/data.json.log :
{"name":"Book A", "genre": "Comedy"}
{"name":"Book B", "genre": "Thriller"}
{"name":"Book C", "genre": "Drama"}
Après la construction, nous pouvons utiliser l'exemple suivant pour l'exécuter (sauts de ligne ajoutés pour plus de lisibilité). Dans notre exemple, deux fichiers seront importés, un depuis le classpath et un depuis le système de fichiers :
java -cp target/spring-boot-persistence-mongodb/WEB-INF/lib/*:target/spring-boot-persistence-mongodb/WEB-INF/classes \
-Djdk.tls.client.protocols=TLSv1.2 \
com.baeldung.SpringBootPersistenceApplication \
--import=classpath:data.json.log \
--import=/tmp/data.json \
--collection=books
4.2. Fichier JSON à partir du téléchargement HTTP POST
De plus, si nous créons un contrôleur REST, nous aurons un point de terminaison pour télécharger et importer des fichiers JSON. Pour cela, nous aurons besoin d'un MultipartFile paramètre :
@RestController
@RequestMapping("/import-json")
public class ImportJsonController {
@Autowired
private ImportJsonService service;
@PostMapping("/file/{collection}")
public String postJsonFile(@RequestPart("parts") MultipartFile jsonStringsFile, @PathVariable String collection) {
List<String> jsonLines = ImportUtils.lines(jsonStringsFile);
return service.importTo(collection, jsonLines);
}
}
Maintenant, nous pouvons importer des fichiers avec un POST comme celui-ci, où "/tmp/data.json ” fait référence à un fichier existant :
curl -X POST http://localhost:8082/import-json/file/books -F "[email protected]/tmp/books.json"
4.3. Mappage de JSON à un type Java spécifique
Nous utilisons uniquement JSON, non lié à aucun type, ce qui est l'un des avantages de travailler avec MongoDB. Nous voulons maintenant valider notre entrée. Dans ce cas, ajoutons un ObjectMapper en apportant cette modification à notre service :
private <T> List<Document> generateMongoDocs(List<String> lines, Class<T> type) {
ObjectMapper mapper = new ObjectMapper();
List<Document> docs = new ArrayList<>();
for (String json : lines) {
if (type != null) {
mapper.readValue(json, type);
}
docs.add(Document.parse(json));
}
return docs;
}
De cette façon, si le type paramètre est spécifié, notre mapper essaiera d'analyser notre chaîne JSON comme ce type. Et, avec la configuration par défaut, lèvera une exception si des propriétés inconnues sont présentes. Voici notre définition de bean simple pour travailler avec un référentiel MongoDB :
@Document("books")
public class Book {
@Id
private String id;
private String name;
private String genre;
// getters and setters
}
Et maintenant, pour utiliser la version améliorée de notre générateur de documents, changeons également cette méthode :
public String importTo(Class<?> type, List<String> jsonLines) {
List<Document> mongoDocs = generateMongoDocs(jsonLines, type);
String collection = type.getAnnotation(org.springframework.data.mongodb.core.mapping.Document.class)
.value();
int inserts = insertInto(collection, mongoDocs);
return inserts + "/" + jsonLines.size();
}
Maintenant, au lieu de passer le nom d'une collection, nous passons une classe . Nous supposons qu'il contient le document annotation telle que nous l'avons utilisée dans notre Livre , afin qu'il puisse récupérer le nom de la collection. Cependant, étant donné que l'annotation et le Document les classes ont le même nom, nous devons spécifier l'ensemble du package.