我使用Spring-Boot2.5.0和MongoDB来持久化一些文档。这里是Github项目。
对于每个文档,我还需要自动保存一些审计信息,因此我扩展了以下类:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.springframework.data.annotation.*;
import java.time.Instant;
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public abstract class AuditingDocument {
@Version
private Long version;
@CreatedBy
private String creator;
@CreatedDate
private Instant created;
@LastModifiedBy
private String modifier;
@LastModifiedDate
private Instant modified;
}
让我们考虑一下Book
类:
@Data
@SuperBuilder
@Document
@NoArgsConstructor
@AllArgsConstructor
public class Book extends AuditingDocument {
@Id
private String id;
private String name;
}
我遇到的问题是,当我通过JSON
/REST API
更新文档时,我能够更改/覆盖@CreatedBy
和@CreatedDate
字段的值。
这意味着如果没有提供字段,则结果值将保存为null,否则,它将为创建者和创建的字段保存新值。
这是不允许的,因为它在大多数用例中都是一个安全问题。如何使这两个字段不可更新?如果创建者存在,则不需要稍后更新它。这些值是自动填充的,因此不需要更新值就不会出现错误。
我发现了其他类似的问题,但它们是关于JPA的,而不是MongoDB。
在这里他们用
@Column(name = "created_by", updatable = false)
若要保护字段免遭更新,请执行以下操作。不幸的是,@Field
for MongoDB没有这样的属性。
在数据库中已经存在所有这样的字段之后,我如何保护它们不被修改?显然,我需要一个能够与所有@Document
实体一起扩展的解决方案,而不需要分别处理每个实体,例如,从DB手动读取它,并首先修复要保存的文档。
更新
我试图通过在一个doUpdate
子类中重写MongoTemplate
方法来实现这种行为。
public class CustomMongoTemplate extends MongoTemplate {
public CustomMongoTemplate(MongoClient mongoClient, String databaseName) {
super(mongoClient, databaseName);
}
public CustomMongoTemplate(MongoDatabaseFactory mongoDbFactory) {
super(mongoDbFactory);
}
public CustomMongoTemplate(MongoDatabaseFactory mongoDbFactory, MongoConverter mongoConverter) {
super(mongoDbFactory, mongoConverter);
}
@Override
protected UpdateResult doUpdate(String collectionName, Query query, UpdateDefinition update, Class<?> entityClass, boolean upsert, boolean multi) {
Document updateDocument = update.getUpdateObject();
List<?> list = this.find(query, entityClass);
if (!list.isEmpty()) {
Object existingObject = list.get(0);
Document existingDocument = new Document();
this.getConverter().write(existingObject, existingDocument);
// Keep the values of the existing document
if (existingDocument.keySet().containsAll(Arrays.asList("version", "creator", "created"))) {
// Long version = existingDocument.getLong("version");
String creator = existingDocument.getString("creator");
Date created = existingDocument.getDate("created");
System.out.println("Creator: " + creator);
System.out.println("Created: " + created);
// updateDocument.put("version", version++);
updateDocument.put("creator", creator);
updateDocument.put("created", created);
System.out.println("Update Document");
System.out.println(updateDocument.toJson());
}
return super.doUpdate(collectionName, query, Update.fromDocument(updateDocument), entityClass, upsert, multi);
} else {
return super.doUpdate(collectionName, query, update, entityClass, upsert, multi);
}
}
}
这种方法部分起作用,这意味着在我调用存储库的save方法之后,update的对象不会覆盖现有的创建者和创建的字段,但是由于某种原因,保存方法返回一个具有空值的对象,用于创建者和已创建的对象,即使在数据库中文档有这样的值。
我还尝试一次获取集合的所有文档,它们的值(创建者、创建的)被正确填充并由API端点返回。doUpdate()
方法似乎搞砸了一些东西,但我无法理解wath。
更新2
每个文档都使用实现此接口的服务保存在DB中,后者只调用MongoRepository的相应的MongoRepository方法。
import org.apache.commons.collections4.IterableUtils;
import org.springframework.data.mongodb.repository.MongoRepository;
import java.util.List;
import java.util.Optional;
public interface EntityService<T, K> {
MongoRepository<T, K> getRepository();
default Optional<T> findById(K id) {
return this.getRepository().findById(id);
}
default List<T> findAll(){
return this.getRepository().findAll();
}
default List<T> findAllByIds(List<K> ids){
return IterableUtils.toList(this.getRepository().findAllById(ids));
}
default T save(T entity) {
return this.getRepository().save(entity);
}
default List<T> save(Iterable<T> entities) {
return this.getRepository().saveAll(entities);
}
default void delete(T entity) {
this.getRepository().delete(entity);
}
default void delete(Iterable<T> entity) {
this.getRepository().deleteAll(entity);
}
}
这是对应的@Repository
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface BookRepository extends MongoRepository<Book, String>, QuerydslPredicateExecutor<Book> {}
更新3
RestController调用此方法,其中服务是上面定义的方法:
default T save(T entity) {
return this.convert(this.getService().save(this.decode(entity)));
}
这些是转换和解码方法:
@Override
public BookDTO convert(Book source) {
return BookDTO.builder()
.id(source.getId())
// Auditing Info
.version(source.getVersion())
.creator(source.getCreator())
.created(source.getCreated())
.modifier(source.getModifier())
.modified(source.getModified())
.build();
}
@Override
public Book decode(BookDTO target) {
return Book.builder()
.id(target.getId())
// Auditing Info
.version(target.getVersion())
// .creator(target.getCreator())
// .created(target.getCreated())
// .modifier(target.getModifier())
// .modified(target.getModified())
.build();
}
更新4
我刚刚创建了Spring /Java16MWP来在GitHub上再现错误。
这是RestController:
@RestController
@RequiredArgsConstructor
public class BookController {
private final BookRepository bookRepository;
@PostMapping(value = "/book")
public Book save(@RequestBody Book entity) {
return this.bookRepository.save(entity);
}
@GetMapping(value = "/book/test")
public Book test() {
Book book = Book.builder().name("Book1").build();
return this.bookRepository.save(book);
}
@GetMapping(value = "/books")
public List<Book> books() {
return this.bookRepository.findAll();
}
}
如果我通过"/book"
端点更新文档,数据库中的文档将被正确保存(与现有的创建者和创建的字段一起),但是Rest Controller将返回这些字段的空值。
但是,"/books"
返回所有正确填充字段的书籍。
在the doUpdate
方法和控制器返回之间似乎存在一些东西,它将这些字段设置为null。
更新5
为了更好地检查BookRepository
的保存方法,我创建了一些测试。
我发现的是:
version, creator, created, modifier, modified
)都按预期填充。creator
和created
字段的现有值。下面是我的测试方法(也可以在GitHub上使用)。
import com.example.demo.domain.Book;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
@SpringBootTest
@Rollback
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class BookRepositoryTests {
@Autowired
private BookRepository bookRepository;
@Test
@Order(1)
@Transactional
public void testCreateBook() {
this.doCreateBook("1001", "Java Programming");
}
@Test
@Order(2)
@Transactional
public void testUpdateBookAndFind() {
this.doCreateBook("1002", "Python Programming");
Book existingBook = this.bookRepository.findById("1002").orElse(null);
// Check Existing Book
Assertions.assertNotNull(existingBook);
// Update
existingBook.setCreated(null);
existingBook.setCreator(null);
existingBook.setModifier(null);
existingBook.setModified(null);
this.bookRepository.save(existingBook);
Book existingUpdatedBook = this.bookRepository.findById("1002").orElse(null);
// Check Existing Updated Book (Working)
Assertions.assertNotNull(existingUpdatedBook);
Assertions.assertNotNull(existingUpdatedBook.getCreator());
Assertions.assertNotNull(existingUpdatedBook.getCreated());
Assertions.assertNotNull(existingUpdatedBook.getModifier());
Assertions.assertNotNull(existingUpdatedBook.getModified());
}
@Test
@Order(3)
@Transactional
public void testUpdateBookDirect() {
this.doCreateBook("1003", "Go Programming");
Book existingBook = this.bookRepository.findById("1003").orElse(null);
// Check Existing Book
Assertions.assertNotNull(existingBook);
// Update
existingBook.setCreated(null);
existingBook.setCreator(null);
existingBook.setModifier(null);
existingBook.setModified(null);
Book updatedBook = this.bookRepository.save(existingBook);
// Check Updated Book (Not working)
Assertions.assertNotNull(updatedBook);
Assertions.assertNotNull(updatedBook.getCreator());
Assertions.assertNotNull(updatedBook.getCreated());
Assertions.assertNotNull(updatedBook.getModifier());
Assertions.assertNotNull(updatedBook.getModified());
}
private void doCreateBook(String bookID, String bookName) {
// Create Book
Book book = Book.builder().id(bookID).name(bookName).build();
Book createdBook = this.bookRepository.save(book);
Assertions.assertNotNull(createdBook);
Assertions.assertEquals(bookID, createdBook.getId());
Assertions.assertEquals(bookName, createdBook.getName());
// Check Auditing Fields
Assertions.assertNotNull(createdBook.getVersion());
Assertions.assertNotNull(createdBook.getCreator());
Assertions.assertNotNull(createdBook.getCreated());
Assertions.assertNotNull(createdBook.getModifier());
Assertions.assertNotNull(createdBook.getModified());
}
}
在综合中,只有testUpdateBookDirect()
方法的断言不起作用。似乎在CustomMongoTemplate.doUpdate()
方法之后就有某种拦截器覆盖了这些字段(创建者,创建的)。
发布于 2021-08-04 07:31:51
一种可能的解决办法或解决办法是:
MongoTemplate
类doUpdate
方法save
和find
方法组合起来,以更新并返回已更新的对象和正确填充的审核字段。这是必需的,因为出于某种原因,repository.save
方法返回审核字段(creator
、created
)的null,即使它们随后正确地填充在DB中。在这里,我们需要覆盖MongoTemplate
的MongoTemplate
方法。
@Override
protected UpdateResult doUpdate(String collectionName, Query query, UpdateDefinition update, Class<?> entityClass, boolean upsert, boolean multi) {
Document updateDocument = update.getUpdateObject();
List<?> list = this.find(query, entityClass);
if (!list.isEmpty()) {
Object existingObject = list.get(0);
Document existingDocument = new Document();
this.getConverter().write(existingObject, existingDocument);
// Keep the values of the existing document
if (existingDocument.keySet().containsAll(Arrays.asList("version", "creator", "created"))) {
String creator = existingDocument.getString("creator");
Date created = existingDocument.getDate("created");
System.out.println("Creator: " + creator);
System.out.println("Created: " + created);
updateDocument.put("creator", creator);
updateDocument.put("created", created);
System.out.println("Update Document");
System.out.println(updateDocument.toJson());
}
return super.doUpdate(collectionName, query, Update.fromDocument(updateDocument), entityClass, upsert, multi);
} else {
return super.doUpdate(collectionName, query, update, entityClass, upsert, multi);
}
}
最后,我使用了一个服务,它调用存储库进行保存和查找操作。这是它实现的接口。
public interface EntityService<T extends MongoDBDocument<K>, K> {
MongoRepository<T, K> getRepository();
default T save(T entity) {
// First save it
this.getRepository().save(entity);
// Then find it by ID
return this.getRepository().findById(entity.getId()).orElse(entity);
}
default List<T> save(Iterable<T> entities) {
// First save them
List<T> savedEntities = this.getRepository().saveAll(entities);
List<K> savedEntitiesIDs = savedEntities.stream().map(entity -> entity.getId()).collect(Collectors.toList());
// Then find them by IDs
return IterableUtils.toList(this.getRepository().findAllById(savedEntitiesIDs));
}
}
这样我就能做我想要做的事:
save
和update
API端点使用相同的DTO接口。发布于 2021-07-21 12:15:22
如果您不希望请求覆盖您的审计字段(或任何其他字段),那么一种方法是为您的数据模型和DTO拥有不同的类,并在进进出出的过程中从一个类转换到另一个类(Lombok构建器使这非常容易)。
虽然转换有一定的开销,加上需要维护这两个类,但它确实将数据模型与面向端点的公共需求隔离开来。
例如,Java在枚举中使用SNAKE_CASE
,但出于某种疯狂的原因,您需要在API上使用kebab-case
。
或者您有一个多租服务,您必须在DB中持久化租户,但不需要或不想通过DTO公开它。
https://stackoverflow.com/questions/68441685
复制相似问题