MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Importa dati in MongoDB dal file JSON utilizzando Java

1. Introduzione

In questo tutorial impareremo come leggere i dati JSON dai file e importarli in MongoDB usando Spring Boot. Questo può essere utile per molte ragioni:ripristino dei dati, inserimento collettivo di nuovi dati o inserimento di valori predefiniti. MongoDB utilizza JSON internamente per strutturare i suoi documenti, quindi, naturalmente, è quello che useremo per archiviare i file importabili. Essendo un testo normale, questa strategia ha anche il vantaggio di essere facilmente comprimibile.

Inoltre, impareremo come convalidare i nostri file di input rispetto ai nostri tipi personalizzati quando necessario. Infine, esporremo un'API in modo da poterla utilizzare durante il runtime nella nostra app Web.

2. Dipendenze

Aggiungiamo queste dipendenze Spring Boot al nostro 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>

Avremo anche bisogno di un'istanza in esecuzione di MongoDB, che richiede un application.properties configurato correttamente file.

3. Importazione di stringhe JSON

Il modo più semplice per importare JSON in MongoDB è convertirlo in un "org.bson.Document " prima l'oggetto. Questa classe rappresenta un documento MongoDB generico di nessun tipo specifico. Pertanto non dobbiamo preoccuparci di creare repository per tutti i tipi di oggetti che potremmo importare.

La nostra strategia prende JSON (da un file, una risorsa o una stringa), lo converte in Documento s e li salva utilizzando MongoTemplate . Le operazioni batch in genere funzionano meglio poiché la quantità di viaggi di andata e ritorno è ridotta rispetto all'inserimento di ogni oggetto singolarmente.

Soprattutto, considereremo che il nostro input abbia un solo oggetto JSON per interruzione di riga. In questo modo, possiamo facilmente delimitare i nostri oggetti. Incapsulare queste funzionalità in due classi che creeremo:ImportUtils e ImportJsonService . Iniziamo con la nostra classe di servizio:

@Service
public class ImportJsonService {

    @Autowired
    private MongoTemplate mongo;
}

Successivamente, aggiungiamo un metodo che analizzi le righe di JSON nei documenti:

private List<Document> generateMongoDocs(List<String> lines) {
    List<Document> docs = new ArrayList<>();
    for (String json : lines) {
        docs.add(Document.parse(json));
    }
    return docs;
}

Quindi aggiungiamo un metodo che inserisce un elenco di Documento oggetti nella raccolta desiderata . Inoltre, è possibile che l'operazione batch non riesca parzialmente. In tal caso, possiamo restituire il numero di documenti inseriti controllando la causa dell'eccezione :

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;
    }
}

Infine, uniamo questi metodi. Questo prende l'input e restituisce una stringa che mostra quante righe sono state lette rispetto a quelle inserite correttamente:

public String importTo(String collection, List<String> jsonLines) {
    List<Document> mongoDocs = generateMongoDocs(jsonLines);
    int inserts = insertInto(collection, mongoDocs);
    return inserts + "/" + jsonLines.size();
}

4. Casi d'uso

Ora che siamo pronti per elaborare l'input, possiamo creare alcuni casi d'uso. Creiamo ImportUtils classe per aiutarci in questo. Questa classe sarà responsabile della conversione dell'input in righe di JSON. Conterrà solo metodi statici. Iniziamo con quello per leggere una semplice Stringa :

public static List<String> lines(String json) {
    String[] split = json.split("[\\r\\n]+");
    return Arrays.asList(split);
}

Dal momento che stiamo usando le interruzioni di riga come delimitatore, regex funziona benissimo per spezzare le stringhe in più righe. Questa espressione regolare gestisce sia le terminazioni di riga di Unix che di Windows. Successivamente, un metodo per convertire un file in un elenco di stringhe:

public static List<String> lines(File file) {
    return Files.readAllLines(file.toPath());
}

Allo stesso modo, finiamo con un metodo per convertire una risorsa del percorso di classe in un elenco:

public static List<String> linesFromResource(String resource) {
    Resource input = new ClassPathResource(resource);
    Path path = input.getFile().toPath();
    return Files.readAllLines(path);
}

4.1. Importa file durante l'avvio con una CLI

Nel nostro primo caso d'uso, implementeremo la funzionalità per l'importazione di un file tramite argomenti dell'applicazione. Approfitteremo di Spring Boot ApplicationRunner interfaccia per farlo all'avvio. Ad esempio, possiamo leggere i parametri della riga di comando per definire il file da importare:

@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);
            }
        }
    }
}

Utilizzo di getOptionValues() possiamo elaborare uno o più file. Questi file possono provenire dal nostro percorso di classe o dal nostro file system. Li differenziamo usando RESOURCE_PREFIX . Ogni argomento che inizia con “classpath: ” verrà letto dalla nostra cartella delle risorse anziché dal file system. Successivamente, verranno tutti importati nella raccolta desiderata .

Iniziamo a usare la nostra applicazione creando un file in src/main/resources/data.json.log :

{"name":"Book A", "genre": "Comedy"}
{"name":"Book B", "genre": "Thriller"}
{"name":"Book C", "genre": "Drama"}

Dopo la creazione, possiamo utilizzare il seguente esempio per eseguirlo (interruzioni di riga aggiunte per la leggibilità). Nel nostro esempio verranno importati due file, uno dal classpath e uno dal file system:

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. File JSON dal caricamento HTTP POST

Inoltre, se creiamo un controller REST, avremo un endpoint per caricare e importare file JSON. Per questo, avremo bisogno di un MultipartFile parametro:

@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);
    }
}

Ora possiamo importare file con un POST come questo, dove “/tmp/data.json ” si riferisce a un file esistente:

curl -X POST http://localhost:8082/import-json/file/books -F "[email protected]/tmp/books.json"

4.3. Mappatura di JSON su un tipo Java specifico

Abbiamo utilizzato solo JSON, non vincolato a nessun tipo, che è uno dei vantaggi di lavorare con MongoDB. Ora vogliamo convalidare il nostro contributo. In questo caso, aggiungiamo un ObjectMapper apportando questa modifica al nostro servizio:

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;
}

In questo modo, se il tipo parametro è specificato, il nostro mapper proverà ad analizzare la nostra stringa JSON come quel tipo. E, con la configurazione predefinita, genererà un'eccezione se sono presenti proprietà sconosciute. Ecco la nostra semplice definizione di bean per lavorare con un repository MongoDB:

@Document("books")
public class Book {
    @Id
    private String id;
    private String name;
    private String genre;
    // getters and setters
}

E ora, per utilizzare la versione migliorata del nostro Generatore di documenti, cambiamo anche questo metodo:

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();
}

Ora, invece di passare il nome di una collezione, passiamo a una Class . Partiamo dal presupposto che contenga il Documento annotazione come abbiamo usato nel nostro Libro , in modo che possa recuperare il nome della raccolta. Tuttavia, poiché sia ​​l'annotazione che il Documento le classi hanno lo stesso nome, dobbiamo specificare l'intero pacchetto.