java - example - Spring Data MongoRepository guardar(T) no funciona... a veces
spring data mongodb (2)
Así que hay una pequeña aplicación Angular + Java + Spring Boot + MongoDB con la que estoy trabajando. Está recibiendo mucha acción últimamente (lea: modificaciones de código), pero las clases de acceso a datos han quedado prácticamente intactas en AFAIK.
Sin embargo, parece que MongoRepository
decidió repentinamente dejar de persistir los cambios que estoy MongoRepository
save()
en la base de datos.
Al inspeccionar mongod.log
esto es lo que veo cuando funciona 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
Y esto es lo que veo cuando no lo hace:
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)
Al hacer una tail -f
1 en el archivo de registro durante la depuración, he visto que esas conexiones aparecen justo cuando mi código llama a findById()
o save()
, por lo que parece que la aplicación puede alcanzar la base de datos.
Este es (más o menos) el código Java relevante:
/* 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);
}
He recorrido esa parte cientos de veces mientras depuraba y todo parece estar bien:
-
findById(id)
devuelve correctamente el objetoBookData original
esperado deBookData original
: marque ✓ -
newData
contiene los valores esperados que se utilizarán para la actualización: verificar ✓ - justo antes de llamar a
save(original)
, eloriginal
se ha modificado correctamente usando losnewData
valores denewData
: verificar ✓ -
save()
ejecuta sin errores: cheque ✓ -
save()
devuelve un nuevoBookData
con valores correctamente actualizados: para mi propia sorpresa, marque ✓ - después de
save()
devuelve, una consultadb.bookData.find()
en Mongo Shell muestra que los valores se han actualizado: falla . - después de que retorna
save()
, el objetoBookData
recuperado por las nuevas llamadas afindById()
contiene los valores actualizados: falla (a veces lo hace, a veces no).
Parece que MongoDB está esperando algún tipo de flush()
, pero este no es un repositorio JPA al que se puede llamar saveAndFlush()
lugar.
Alguna idea de por qué esto esta pasando?
EDITAR: versiones (según lo solicitado):
- Java 8
- Bota de primavera 1.4.5
- MongoDB 3.2.6
- Windows 10
También BookData
arriba.
MongoDB es inherentemente una tienda de caché, por lo que quiero decir, no se garantiza que los contenidos sean los últimos o necesariamente correctos. No he podido encontrar las opciones de configuración para el tiempo de descarga (pero se configurarían en la propia base de datos), pero MongoDB ha agregado funciones para que pueda elegir rápido + sucio, o lento + limpio. Este factor de "frescura" es probablemente su problema si está viendo este tipo de problema. (Incluso si no está ejecutando la distribución, hay una diferencia de tiempo entre el reconocimiento de la solicitud y la solicitud confirmada)
Aquí hay un enlace para publicar sobre "lectura limpia" (punto clave en la siguiente cita)
http://www.dagolden.com/index.php/2633/no-more-dirty-reads-with-mongodb/
Recomiendo a los usuarios de MongoDB que se coloquen (o al menos, sus actividades de aplicación) en uno de los siguientes grupos:
"Quiero baja latencia": las lecturas sucias están bien siempre que las cosas sean rápidas. Use w = 1 y lea la preocupación ''local''. (Estas son las configuraciones predeterminadas). "Quiero consistencia": las lecturas sucias no están bien, incluso a costa de la latencia o datos ligeramente desactualizados. Use w = "mayoría" y lea la mayoría de "preocupación". utilizar MongoDB v1.2.0;
my $mc = MongoDB->connect( $uri, { read_concern_level => ''majority'', w => ''majority'', } );
Lecturas adicionales que pueden o no ser útiles.
- https://docs.mongodb.com/manual/faq/concurrency/
- https://www.rainforestqa.com/blog/2012-11-05-mongodb-gotchas-and-how-to-avoid-them/ (A saber, la sección de registro en diario)
Actualizar
Si se ejecuta en un entorno de subprocesos múltiples, asegúrese de que sus subprocesos no estén pisoteando las actualizaciones de otro. Puede verificar si esto ocurre configurando el sistema o el nivel de registro de consultas en 5. https://docs.mongodb.com/manual/reference/log-messages/#log-messages-configure-verbosity
Problema resuelto.
Una llamada asíncrona diferente del cliente JS, a un punto final diferente en el backend de Java, estaba sobrescribiendo mi documento actualizado en un hilo diferente con los valores originales.
Ambas operaciones de actualización llamaban a findById
antes de guardar. El problema fue que lo hicieron al mismo tiempo, por lo que obtuvieron los mismos valores originales.
Cada uno continuó con la actualización de sus campos relevantes y con la opción de save
al final, lo que provocó que el otro subproceso anulara mis cambios.
Cada llamada se registró solo con los campos modificados relevantes, por lo que no me di cuenta de que uno de ellos estaba sobrescribiendo los cambios del otro.
Una vez que agregué systemLog.verbosity: 3
al config.cfg
de MongoDB para que registrara todas las operaciones, quedó claro que 2 operaciones WRITE diferentes se estaban realizando al mismo tiempo (con una diferencia de ~ 500 ms) pero con valores diferentes.
Luego solo fue cuestión de mover el findById
más cerca del save
y garantizar que las llamadas de JS se hicieran en orden (haciendo que una de las promesas dependiera de la otra).
En retrospectiva, esto probablemente no habría ocurrido si usé MongoOperations
o MongoTemplate
, que ofrecen métodos de update
única y findAndModify
que también permiten operaciones de campo único, en lugar de MongoRepository
donde me veo obligado a hacerlo en 3 pasos ( find
, modificar entidad devuelta, save
) y para trabajar con el documento completo.
EDITAR: Realmente no me gustó mi primer findById
"mudar findById
más cerca de save
", así que al final hice lo que sentí que era correcto e implementé métodos de guardado personalizados que usaban la API de update
más detallada de MongoTemplate
. Código final:
/* 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);
Al hacerlo, cada punto final puede update
con la frecuencia que sea necesaria a través de MongoTemplate
sin tener que preocuparse de sobrescribir campos no relacionados, y aún MongoRepository
métodos MongoRepository
basados en MongoRepository
para cosas como la creación de nuevas entidades, los métodos @Query
, los @Query
anotados, etc.