sql >> Databasteknik >  >> NoSQL >> MongoDB

Importera data till MongoDB från JSON-fil med Java

1. Inledning

I den här handledningen kommer vi att lära oss hur man läser JSON-data från filer och importerar dem till MongoDB med Spring Boot. Detta kan vara användbart av många anledningar:återställning av data, massinsättning av ny data eller infogning av standardvärden. MongoDB använder JSON internt för att strukturera sina dokument, så det är naturligtvis vad vi kommer att använda för att lagra importerbara filer. Eftersom den här strategin är ren text har den också fördelen att den är lätt att komprimera.

Dessutom kommer vi att lära oss hur vi validerar våra indatafiler mot våra anpassade typer vid behov. Slutligen kommer vi att exponera ett API så att vi kan använda det under körning i vår webbapp.

2. Beroenden

Låt oss lägga till dessa Spring Boot-beroenden till vår 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>

Vi kommer också att behöva en körande instans av MongoDB, som kräver en korrekt konfigurerad application.properties fil.

3. Importera JSON-strängar

Det enklaste sättet att importera JSON till MongoDB är att konvertera den till ett "org.bson.Document ” objekt först. Den här klassen representerar ett generiskt MongoDB-dokument av ingen specifik typ. Därför behöver vi inte oroa oss för att skapa arkiv för alla typer av objekt vi kan importera.

Vår strategi tar JSON (från en fil, resurs eller sträng), konverterar den till Dokument s och sparar dem med MongoTemplate . Batchoperationer fungerar generellt bättre eftersom antalet tur- och returresor minskar jämfört med att infoga varje objekt individuellt.

Viktigast av allt kommer vi att anse att vår input bara har ett JSON-objekt per radbrytning. På så sätt kan vi enkelt avgränsa våra objekt. Vi kapslar in dessa funktioner i två klasser som vi skapar:ImportUtils och ImportJsonService . Låt oss börja med vår serviceklass:

@Service
public class ImportJsonService {

    @Autowired
    private MongoTemplate mongo;
}

Låt oss sedan lägga till en metod som analyserar rader av JSON till dokument:

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

Sedan lägger vi till en metod som infogar en lista med Dokument objekt till önskad samling . Det är också möjligt att batchoperationen delvis misslyckas. I så fall kan vi returnera antalet infogade dokument genom att kontrollera orsaken av undantaget :

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

Låt oss slutligen kombinera dessa metoder. Den här tar indata och returnerar en sträng som visar hur många rader som lästes kontra framgångsrikt infogade:

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

4. Användningsfall

Nu när vi är redo att bearbeta indata kan vi bygga några användningsfall. Låt oss skapa ImportUtils klass för att hjälpa oss med det. Den här klassen kommer att ansvara för att konvertera indata till rader av JSON. Den kommer bara att innehålla statiska metoder. Låt oss börja med den för att läsa en enkel sträng :

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

Eftersom vi använder radbrytningar som avgränsare fungerar regex utmärkt för att dela upp strängar i flera rader. Detta regex hanterar både Unix- och Windows-radändelser. Därefter en metod för att konvertera en fil till en lista med strängar:

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

På liknande sätt avslutar vi med en metod för att konvertera en klassvägsresurs till en lista:

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

4.1. Importera fil under uppstart med en CLI

I vårt första användningsfall kommer vi att implementera funktionalitet för att importera en fil via applikationsargument. Vi kommer att dra nytta av Spring Boot ApplicationRunner gränssnitt för att göra detta vid uppstart. Vi kan till exempel läsa kommandoradsparametrar för att definiera filen som ska importeras:

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

Använder getOptionValues() vi kan behandla en eller flera filer. Dessa filer kan vara antingen från vår klasssökväg eller från vårt filsystem. Vi skiljer dem åt med RESOURCE_PREFIX . Varje argument som börjar med "classpath: ” kommer att läsas från vår resursmapp istället för från filsystemet. Efter det kommer de alla att importeras till önskad samling .

Låt oss börja använda vår applikation genom att skapa en fil under src/main/resources/data.json.log :

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

Efter byggandet kan vi använda följande exempel för att köra det (radbrytningar har lagts till för läsbarhet). I vårt exempel kommer två filer att importeras, en från klasssökvägen och en från filsystemet:

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. JSON-fil från HTTP POST-uppladdning

Dessutom, om vi skapar en REST-kontroller, har vi en slutpunkt för att ladda upp och importera JSON-filer. För det behöver vi en MultipartFile parameter:

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

Nu kan vi importera filer med en POST som denna, där "/tmp/data.json ” refererar till en befintlig fil:

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

4.3. Mappa JSON till en specifik Java-typ

Vi har bara använt JSON, inte bundet till någon typ, vilket är en av fördelarna med att arbeta med MongoDB. Nu vill vi validera vår input. I det här fallet lägger vi till en ObjectMapper genom att göra denna ändring i vår tjänst:

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

På så sätt, om typen parametern är specificerad, vår mappare kommer att försöka analysera vår JSON-sträng som den typen. Och, med standardkonfiguration, ger ett undantag om några okända egenskaper finns. Här är vår enkla böndefinition för att arbeta med ett MongoDB-förråd:

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

Och nu, för att använda den förbättrade versionen av vår dokumentgenerator, låt oss ändra denna metod också:

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

Nu, istället för att skicka namnet på en samling, passerar vi en Klass . Vi antar att den har dokumentet anteckning som vi använde i vår bok , så att den kan hämta samlingens namn. Men eftersom både anteckningen och Dokumentet klasser har samma namn, vi måste ange hela paketet.


  1. Go JSON-avkodning är mycket långsam. Vad skulle vara ett bättre sätt att göra det?

  2. Hierarkiska frågor med Mongo med $graphLookup

  3. Redis Cross Slot-fel

  4. Hur såddar jag en mongodatabas med docker-compose?