sql >> Base de Datos >  >> NoSQL >> MongoDB

Importar datos a MongoDB desde un archivo JSON usando Java

1. Introducción

En este tutorial, aprenderemos cómo leer datos JSON de archivos e importarlos a MongoDB usando Spring Boot. Esto puede ser útil por muchas razones:restauración de datos, inserción masiva de nuevos datos o inserción de valores predeterminados. MongoDB usa JSON internamente para estructurar sus documentos, así que, naturalmente, eso es lo que usaremos para almacenar archivos importables. Al ser texto sin formato, esta estrategia también tiene la ventaja de ser fácilmente comprimible.

Además, aprenderemos cómo validar nuestros archivos de entrada contra nuestros tipos personalizados cuando sea necesario. Finalmente, expondremos una API para que podamos usarla durante el tiempo de ejecución en nuestra aplicación web.

2. Dependencias

Agreguemos estas dependencias de Spring Boot a nuestro 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>

También vamos a necesitar una instancia en ejecución de MongoDB, que requiere una application.properties correctamente configurada. archivo.

3. Importación de cadenas JSON

La forma más sencilla de importar JSON a MongoDB es convertirlo en un "org.bson.Document ” objeto primero. Esta clase representa un documento MongoDB genérico sin un tipo específico. Por lo tanto, no tenemos que preocuparnos por crear repositorios para todo tipo de objetos que podamos importar.

Nuestra estrategia toma JSON (de un archivo, recurso o cadena), lo convierte en Documento s, y los guarda usando MongoTemplate . Las operaciones por lotes generalmente funcionan mejor ya que la cantidad de viajes de ida y vuelta se reduce en comparación con la inserción de cada objeto individualmente.

Lo que es más importante, consideraremos que nuestra entrada tiene solo un objeto JSON por salto de línea. De esa manera, podemos delimitar fácilmente nuestros objetos. Encapsularemos estas funcionalidades en dos clases que crearemos:ImportUtils y ImportarJsonService . Comencemos con nuestra clase de servicio:

@Service
public class ImportJsonService {

    @Autowired
    private MongoTemplate mongo;
}

A continuación, agreguemos un método que analice líneas de JSON en documentos:

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

Luego agregamos un método que inserta una lista de Documento objetos en la colección deseada . Además, es posible que la operación por lotes falle parcialmente. En ese caso, podemos devolver el número de documentos insertados comprobando la causa de la excepción :

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

Finalmente, combinemos esos métodos. Este toma la entrada y devuelve una cadena que muestra cuántas líneas se leyeron y cuántas líneas se insertaron correctamente:

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

4. Casos de uso

Ahora que estamos listos para procesar la entrada, podemos crear algunos casos de uso. Vamos a crear ImportUtils clase para ayudarnos con eso. Esta clase será responsable de convertir la entrada en líneas de JSON. Solo contendrá métodos estáticos. Comencemos con el de leer una simple String :

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

Dado que estamos usando saltos de línea como delimitadores, regex funciona muy bien para dividir cadenas en varias líneas. Esta expresión regular maneja los finales de línea de Unix y Windows. A continuación, un método para convertir un archivo en una lista de cadenas:

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

De manera similar, terminamos con un método para convertir un recurso de classpath en una lista:

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

4.1. Importar archivo durante el inicio con una CLI

En nuestro primer caso de uso, implementaremos la funcionalidad para importar un archivo a través de los argumentos de la aplicación. Aprovecharemos Spring Boot ApplicationRunner interfaz para hacer esto en el momento del arranque. Por ejemplo, podemos leer los parámetros de la línea de comando para definir el archivo a importar:

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

Usando getOptionValues() podemos procesar uno o más archivos. Estos archivos pueden ser de nuestra classpath o de nuestro sistema de archivos. Los diferenciamos usando el RESOURCE_PREFIX . Cada argumento que comienza con "classpath: ” se leerá desde nuestra carpeta de recursos en lugar de desde el sistema de archivos. Después de eso, todos se importarán a la colección deseada. .

Comencemos a usar nuestra aplicación creando un archivo en src/main/resources/data.json.log :

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

Después de compilar, podemos usar el siguiente ejemplo para ejecutarlo (se agregaron saltos de línea para mejorar la legibilidad). En nuestro ejemplo, se importarán dos archivos, uno del classpath y otro del sistema de archivos:

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. Archivo JSON desde HTTP POST Carga

Además, si creamos un controlador REST, tendremos un punto final para cargar e importar archivos JSON. Para eso, necesitaremos un MultipartFile parámetro:

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

Ahora podemos importar archivos con un POST como este, donde “/tmp/data.json ” se refiere a un archivo existente:

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

4.3. Asignación de JSON a un tipo de Java específico

Hemos estado usando solo JSON, no vinculado a ningún tipo, lo cual es una de las ventajas de trabajar con MongoDB. Ahora queremos validar nuestra entrada. En este caso, agreguemos un ObjectMapper al hacer este cambio en nuestro servicio:

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 esa forma, si el tipo se especifica el parámetro, nuestro mapper intentará analizar nuestra cadena JSON como ese tipo. Y, con la configuración predeterminada, lanzará una excepción si hay propiedades desconocidas presentes. Aquí está nuestra definición simple de bean para trabajar con un repositorio MongoDB:

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

Y ahora, para usar la versión mejorada de nuestro generador de documentos, cambiemos este método también:

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

Ahora, en lugar de pasar el nombre de una colección, pasamos una Clase . Suponemos que tiene el Documento anotación como la que usamos en nuestro Libro , para que pueda recuperar el nombre de la colección. Sin embargo, dado que tanto la anotación como el Documento las clases tienen el mismo nombre, tenemos que especificar todo el paquete.