web-dev-qa-db-ja.com

Spring Data MongoRepository save(T)が機能しない...時々

だからこの小さなAngular + Java + Spring Boot +私が作業しているMongoDBアプリ。かなりのアクションを得ている。最近ではありますが、データアクセスクラスはほとんど手付かずのままです。
しかし、MongoRepositoryは突然、私がsave() ingしている変更をDBに永続化するのをやめることに決めたようです。

_mongod.log_を調べると、これはsave()が機能するときに表示されます。

_2018-04-11T15:04:06.840+0200 I COMMAND  [conn6] command pdfviewer.bookData command: find { find: "bookData", filter: { _id: "ID_1" }, limit: 1, singleBatch: true } planSummary: IDHACK keysExamined:1 docsExamined:1 idhack:1 cursorExhausted:1 keyUpdates:0 writeConflicts:0 numYields:1 nreturned:1 reslen:716 locks:{ Global: { acquireCount: { r: 4 } }, Database: { acquireCount: { r: 2 } }, Collection: { acquireCount: { r: 2 } } } protocol:op_query 102ms
2018-04-11T17:30:19.615+0200 I WRITE    [conn7] update pdfviewer.bookData query: { _id: "ID_1" } update: { _class: "model.BookData", _id: "ID_1", config: { mode: "normal", offlineEnabled: true }, metadata: { title: "PDFdePrueba3pag   copia  6 ", ...}, downloaded: false, currentPageNumber: 2, availablePages: 3, bookmarks: [], stats: { _id: "c919e517-3c68-462c-8396-d4ba391762e6", dateOpen: new Date(1523460575872), dateClose: new Date(1523460575951), timeZone: "+2", ... }, ... } keysExamined:1 docsExamined:1 nMatched:1 nModified:1 keyUpdates:0 writeConflicts:1 numYields:1 locks:{ Global: { acquireCount: { r: 2, w: 2 } }, Database: { acquireCount: { w: 2 } }, Collection: { acquireCount: { w: 2 } } } 315ms
2018-04-11T17:30:19.615+0200 I COMMAND  [conn7] command pdfviewer.$cmd command: update { update: "bookData", ordered: false, updates: [ { q: { _id: "ID_1" }, u: { _class: "model.BookData", _id: "ID_1", config: { mode: "normal", offlineEnabled: true }, metadata: { title: "PDFdePrueba3pag   copia  6 ", ...}, downloaded: false, currentPageNumber: 2, availablePages: 3, bookmarks: [], stats: { _id: "c919e517-3c68-462c-8396-d4ba391762e6", dateOpen: new Date(1523460575872), dateClose: new Date(1523460575951), timeZone: "+2", ... }, ... }, upsert: true } ] } keyUpdates:0 writeConflicts:0 numYields:0 reslen:55 locks:{ Global: { acquireCount: { r: 2, w: 2 } }, Database: { acquireCount: { w: 2 } }, Collection: { acquireCount: { w: 2 } } } protocol:op_query 316ms
_

そして、これはそうでないときに私が見るものです:

_2018-04-11T18:13:21.864+0200 I NETWORK  [initandlisten] connection accepted from 127.0.0.1:64271 #1 (1 connection now open)
2018-04-11T18:18:51.425+0200 I NETWORK  [initandlisten] connection accepted from 127.0.0.1:64329 #2 (2 connections now open)
2018-04-11T18:19:06.967+0200 I NETWORK  [initandlisten] connection accepted from 127.0.0.1:64346 #3 (3 connections now open)
_

_tail -f_を実行することにより1 デバッグ中のログファイルでは、コードがfindById()またはsave()を呼び出したときにこれらの接続が表示されるので、アプリがDBにアクセスできるようです。

これは(多かれ少なかれ)関連するJavaコード:

_/* BookData.Java */
@Document
public class BookData {

    @Id private String id;
    // Some more non-Id Strings...
    private Config config;
    private Metadata metadata;
    private Boolean downloaded;
    private Integer currentPageNumber;
    private int availablePages;
    private List<Bookmark> bookmarks;
    private StatsModel stats;

    @Transient private byte[] contents;

    public BookData() {}

    // getters and setters
}

/* BookDataRepository.Java */
// MongoRepository comes from spring-boot-starter-parent-1.4.5.RELEASE
public interface BookDataRepository extends MongoRepository<BookData, String> {
    BookData findById(String id);
}

/* BookDataServiceImpl.Java */
public BookData updateBookData(String id, BookData newData) {
    final BookData original = bookDataRepository.findById(id);
    if (original == null) {
        return null;
    }
    original.setCurrentPageNumber(Optional.ofNullable(newData.getCurrentPageNumber()).orElseGet(original::getCurrentPageNumber));
    // similar code for a couple other fields

    return bookDataRepository.save(original);
}
_

デバッグ中にその部分を100回実行しましたが、すべて問題ないようです。

  • findById(id)は期待される_BookData original_オブジェクトを正しく返します:チェック✓
  • newDataには、更新に使用される期待値が含まれています。チェック✓
  • save(original)を呼び出す直前に、original値を使用してnewDataが正しく変更されました:チェック✓
  • save()はエラーなしで実行されます:チェック✓
  • save()は、正しく更新された値を持つ新しいBookDataを返します。驚いたことに、チェック✓
  • save()が戻った後、Mongo Shellのdb.bookData.find()クエリは、値が更新されたことを示します:fail
  • save()が戻った後、findById()への新しい呼び出しによって取得されたBookDataオブジェクトには、更新された値が含まれます。fail(場合によっては、ありません)。

MongoDBが何らかのflush()を待っているように見えますが、これはsaveAndFlush()を代わりに呼び出すことができるJPAリポジトリではありません。

なぜこれが起こっているのでしょうか?

編集:バージョン(要求に応じて):

  • Java 8
  • スプリングブート1.4.5
  • MongoDB 3.2.6
  • ウィンドウズ10

上記のBookDataも含めました。

21
walen

問題が解決しました。
JSクライアントから、Javaバックエンドの異なるエンドポイント)への異なる非同期呼び出しが、異なるスレッドの更新されたドキュメントを元の値で上書きしていました。

両方の更新操作は、保存する前にfindByIdを呼び出していました。問題は、彼らが同時にそうしたことであり、それで彼らは同じ元の値を得ていた。
それぞれの関連フィールドを更新し、最後にsaveを呼び出して、他のスレッドが私の変更を効果的にオーバーライドしました。
各呼び出しは関連する変更されたフィールドのみで記録されたため、一方が他方の変更を上書きしていることに気づきませんでした。

systemLog.verbosity: 3をMongoDBのconfig.cfgに追加してall操作を記録すると、2つの異なるWRITE操作が発生していることが明らかになりました。同じ時間(〜500ミリ秒間隔)が異なる値を使用します。
それから、findByIdsaveに近づけて、JS呼び出しが順番どおりに行われるようにするだけでした(約束の1つを他の約束に依存させる)。

後知恵では、単一フィールドを許可する単一のMongoOperationsおよびMongoTemplateメソッドを提供するupdateまたはfindAndModifyを使用した場合、これはおそらく発生しなかったでしょう。 MongoRepositoryの代わりに、3つのステップ(find、返されたエンティティの変更、save)で強制的に実行し、ドキュメント全体を操作する必要がある操作。


編集:私は最初の「findByIdsaveに近づける」アプローチが本当に好きではなかったので、最終的に私は正しいと感じたことをしましたカスタム保存メソッドを実装しましたMongoTemplateのより詳細なupdate AP​​Iを使用。最終コード:

/* MongoRepository provides entity-based default Spring Data methods */
/* BookDataRepositoryCustom provides field-level update methods */
public interface BookDataRepository extends MongoRepository<BookData, String>, BookDataRepositoryCustom {

    BookData findById(String id);

}

/* Interface for the custom methods */
public interface BookDataRepositoryCustom {

    int saveCurrentPage(String id, Integer currentPage);
}

/* Custom implementation using MongoTemplate. */
@SuppressWarnings("unused")
public class BookDataRepositoryImpl implements BookDataRepositoryCustom {
    @Inject
    MongoTemplate mongoTemplate;

    @Override
    public int saveCurrentPage(String id, Integer currentPage) {
        Query query = new Query(Criteria.where("_id").is(id));
        Update update = new Update();
        update.set("currentPage", currentPage);

        WriteResult result = mongoTemplate.updateFirst(query, update, BookData.class);

        return result == null ? 0 : result.getN();
    }
}

// Old code: get entity from DB, update, save. 3 steps with plenty of room for interferences.
//        BookData bookData = bookDataRepository.findById(bookDataId);
//        bookData.setCurrentPage(currentPage);
//        bookDataRepository.save(bookData);
// New code: update single field. 1 step, 0 problems.
        bookDataRepository.saveCurrentPage(bookDataId, currentPage);

そうすることで、各エンドポイントはupdateを介して必要に応じてMongoTemplateを介して関連のないフィールドを上書きすることを心配することなくでき、エンティティベースのMongoRepositoryメソッドを新しいエンティティの作成、findByメソッド、注釈付き@Querysなど。

7
walen

MongoDBは本質的にキャッシュストアです。つまり、コンテンツが最新であるとは限らないか、必ずしも正しいとは限りません。フラッシュ時間の構成オプションは見つかりませんでした(ただし、DB自体で構成されます)が、MongoDBには、fast + dirtyまたはslow + cleanを選択できる機能が追加されています。この種の問題が発生している場合、この「鮮度」要因が問題である可能性が高くなります。 (分散を実行していない場合でも、リクエストの確認応答とコミットされたリクエストにはタイミングの違いがあります)

「クリーンリーディング」に関する投稿へのリンクを次に示します(次の引用のキーポイント)

http://www.dagolden.com/index.php/2633/no-more-dirty-reads-with-mongodb/

MongoDBユーザーは、自分自身(または少なくとも、アプリケーションアクティビティ)を次のグループのいずれかに配置することをお勧めします。

「待ち時間を短くしたい」–処理が高速であれば、ダーティリードは問題ありません。 w = 1を使用して、懸念事項「ローカル」を読み取ります。 (これらはデフォルトの設定です。)「一貫性が必要です」-遅延やわずかに古いデータを使用しても、ダーティリードは問題ありません。 w = 'majority'を使用し、懸念事項 'majority'を読み取ります。 MongoDB v1.2.0を使用します。

my $mc = MongoDB->connect(
    $uri,
    {
        read_concern_level => 'majority',
        w => 'majority',
    }
);

役に立つかもしれないし、そうでないかもしれないさらなる読書

更新

マルチスレッド環境で実行している場合、スレッドが他のスレッドの更新を踏みつけていないことを確認してください。これが発生しているかどうかを確認するには、システムまたはクエリのログレベルを5に設定します。 https://docs.mongodb.com/manual/reference/log-messages/#log-messages-configure-verbosity =

4
Tezra