1. 序章

このチュートリアルでは、ファイルからJSONデータを読み取り、Spring Bootを使用してMongoDBにインポートする方法を学習します。 これは、データの復元、新しいデータの一括挿入、デフォルト値の挿入など、さまざまな理由で役立ちます。 MongoDBは内部でJSONを使用してドキュメントを構造化するため、当然、インポート可能なファイルの保存に使用します。 プレーンテキストであるため、この戦略には簡単に圧縮可能であるという利点もあります。

さらに、必要に応じて、カスタムタイプに対して入力ファイルを検証する方法を学習します。 最後に、APIを公開して、実行時にWebアプリで使用できるようにします。

2. 依存関係

これらのSpring Boot依存関係を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>

また、MongoDBの実行中のインスタンスが必要になります。これには、適切に構成されたapplication.propertiesファイルが必要です。

3. JSON文字列のインポート

JSONをMongoDBにインポートする最も簡単な方法は、最初にそれを「org.bson.Document」オブジェクトに変換することです。このクラスは、特定のタイプのない一般的なMongoDBドキュメントを表します。 したがって、インポートする可能性のあるすべての種類のオブジェクトのリポジトリを作成することを心配する必要はありません。

私たちの戦略は、JSON(ファイル、リソース、または文字列から)を取得し、それを Document に変換し、MongoTemplateを使用して保存します。 各オブジェクトを個別に挿入する場合に比べてラウンドトリップの量が少なくなるため、バッチ操作のパフォーマンスは一般的に向上します。

最も重要なことは、入力が改行ごとに1つのJSONオブジェクトのみを持っていると見なすことです。 そうすれば、オブジェクトを簡単に区切ることができます。 これらの機能を、作成する2つのクラスImportUtilsImportJsonServiceにカプセル化します。 サービスクラスから始めましょう:

@Service
public class ImportJsonService {

    @Autowired
    private MongoTemplate mongo;
}

次に、JSONの行をドキュメントに解析するメソッドを追加しましょう。

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

次に、Documentオブジェクトのリストを目的のコレクションに挿入するメソッドを追加します。 また、バッチ操作が部分的に失敗する可能性があります。 その場合、例外原因を確認することで、挿入されたドキュメントの数を返すことができます。

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

最後に、これらのメソッドを組み合わせてみましょう。 これは入力を受け取り、読み取られた行数と読み取られた行数を示す文字列を返します。 正常に挿入されました:

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

4. ユースケース

入力を処理する準備ができたので、いくつかのユースケースを作成できます。 それを支援するためにImportUtilsクラスを作成しましょう。 このクラスは、入力をJSONの行に変換する役割を果たします。静的メソッドのみが含まれます。 単純なStringを読み取るためのものから始めましょう。

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

区切り文字として改行を使用しているため、regexは文字列を複数の行に分割するのに最適です。 この正規表現は、UnixとWindowsの両方の行末を処理します。 次に、 Fileを文字列のリストに変換するメソッド:

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

同様に、クラスパスリソースをリストに変換するメソッドで終了します。

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

4.1. CLIを使用した起動時にファイルをインポートする

最初のユースケースでは、アプリケーション引数を介してファイルをインポートするための機能を実装します。 Spring Boot ApplicationRunner インターフェースを利用して、起動時にこれを実行します。 たとえば、コマンドラインパラメーターを読み取って、インポートするファイルを定義できます。

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

getOptionValues()を使用して、1つ以上のファイルを処理できます。 これらのファイルは、クラスパスまたはファイルシステムのいずれかから取得できます。 RESOURCE_PREFIXを使用して区別します。 「classpath:」で始まるすべての引数は、ファイルシステムからではなく、リソースフォルダーから読み取られます。 その後、それらはすべて目的のコレクションにインポートされます。

src / main / resources / data.json.log の下にファイルを作成して、アプリケーションの使用を開始しましょう。

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

ビルドの後、次の例を使用して実行できます(読みやすくするために改行が追加されています)。 この例では、2つのファイルがインポートされます。1つはクラスパスから、もう1つはファイルシステムからです。

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. HTTPPOSTアップロードからのJSONファイル

さらに、 RESTコントローラーを作成すると、JSONファイルをアップロードおよびインポートするためのエンドポイントが作成されます。 そのためには、MultipartFileパラメーターが必要です。

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

これで、次のような POST を使用してファイルをインポートできます。ここで、「/tmp/data.json」は既存のファイルを指します。

curl -X POST http://localhost:8082/import-json/file/books -F "parts=@/tmp/books.json"

4.3. JSONを特定のJavaタイプにマッピングする

私たちはJSONのみを使用しており、どのタイプにもバインドされていません。これは、MongoDBを使用する利点の1つです。 ここで、入力を検証します。この場合、サービスに次の変更を加えて、ObjectMapperを追加しましょう。

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

このように、 type パラメーターが指定されている場合、mapperはJSON文字列をそのタイプとして解析しようとします。 また、デフォルトの構成では、不明なプロパティが存在する場合は例外がスローされます。MongoDBリポジトリを操作するための簡単なbean定義は次のとおりです。

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

そして今、私たちのドキュメントジェネレータの改良版を使用するために、このメソッドも変更しましょう:

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

これで、コレクションの名前を渡す代わりに、Classを渡します。 Book で使用したように、 Document アノテーションが付いていると想定しているため、コレクション名を取得できます。 ただし、アノテーションクラスと Document クラスはどちらも同じ名前であるため、パッケージ全体を指定する必要があります。

5. 結論

この記事では、ファイル、リソース、または単純な文字列からのJSON入力を分割し、それらをMongoDBにインポートする方法について説明しました。 この機能をサービスクラスとユーティリティクラスに一元化して、どこでも再利用できるようにしました。 私たちのユースケースには、CLIとRESTオプション、およびその使用方法に関するコマンド例が含まれていました。

そしていつものように、ソースコードはGitHub利用できます。