当前位置: 首页 > 知识库问答 >
问题:

Spring数据MongoDB:如何避免@CreatedBy和@CreatedDate字段更新?

楮自珍
2023-03-14

我正在使用Spring-Boot 2.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更新文档时,我能够更改/覆盖CreatedBy和CreatedDate字段的值。

这意味着,如果未提供字段,则结果值将保存为null,否则,它将为创建者和创建的字段保存新值。

这不应该被允许,因为它在大多数用例中代表了一个安全问题。如何使这两个字段不可更新?如果创建者存在,则无需稍后更新它。此类值会自动填充,因此不会出现需要更新值的错误。

我发现了其他类似的问题,但它们是关于JPA而不是MongoDB,例如。

  • @Createdby@CreatedDate在实体更新后为空(JPA)

他们在这里使用

@Column(name = "created_by", updatable = false)

保护字段不受更新影响。不幸的是,MongoDB的字段没有这样的属性。

在数据库中已经存在这些字段之后,如何保护这些字段不被修改?显然,我需要一个能够扩展所有文档实体的解决方案,而无需单独处理每个实体,例如从DB手动读取并修复要首先保存的文档。

更新

我试图通过覆盖MongoTemboard子类中的doUpdate方法来实现这种行为。

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

这种方法部分有效,这意味着在我调用存储库的保存方法后,更新的对象不会覆盖现有的创建者和创建的字段,但是由于某种原因,保存方法返回一个创建者和创建的空值对象,即使在数据库中文档有这样的值。

我还尝试一次获取集合的所有文档,它们的值(creator、created)由APIendpoint正确填充和返回。似乎doUpdate()方法弄乱了一些东西,但我无法使用wath。

更新2

每个文档都使用实现此接口的Service保存在DB中,该服务只需调用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);
    }
}

这是相应的存储库

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 Boot/Java 16 MWP,以便在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();
    }
}

如果我通过endpoint更新文档,则数据库中的文档会正确保存(使用现有创建者

但是,“/图书”会返回所有正确填充了所有字段的图书。

doUpdate方法和控制器返回之间似乎存在某种东西,将这些字段设置为null。

更新5

为了更好地检查BookRepository的保存方法,我创建了一些测试。

我发现:

  1. 保存方法在第一次正确地创建了这本书,所有的审核域(version, creator,创建,修改,修改)都按预期填充。
  2. 保存方法正确更新数据库中的现有账本,保留创建者创建的字段的现有值以供后续查找查询。
  3. 保存方法返回一个对象,其创建者和创建的字段设置为null(但在DB中,文档填充了所有审计字段)。

以下是我的测试方法(也可在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()覆盖这些字段的方法(creator,created)。

共有2个答案

燕成双
2023-03-14

如果您不希望请求覆盖审计字段(或任何其他字段),那么一种方法是为您的数据模型和DTO使用不同的类,并在输入和输出的过程中从一个类转换到另一个类(Lombok构建器使这非常容易)。

虽然转换有一定的开销,再加上需要维护这两个类,但它确实将您的数据模型与endpoint面向公众的需求隔离开来。

e、 g.Java对枚举使用了SNAKE\u CASE,但出于某些疯狂的原因,您需要在API上使用kebab CASE。

或者,您有一个多租户服务,您必须在DB中持久化租户,但不需要或不想通过DTO公开它。

姜志行
2023-03-14

一种可能的解决方案或解决方法是:

  1. 扩展MongoTemplate类
  2. 覆盖更新方法
  3. 将存储库的“保存”和“查找”方法结合起来,以更新并返回已更新的对象,并正确填充审核字段。这是必需的,因为出于某种原因,存储库。save方法为审核字段(creatorcreated返回null,即使它们随后在数据库中正确填充

这里我们需要覆盖MongoTemboarddoUpdate方法。

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

通过这种方式,我能够做我正在寻找的事情:

  1. 让be自动生成审核字段(版本、创建者、已创建、修改器、已修改)
 类似资料:
  • 我在spring data jdbc中使用简单的crud操作和审计。因此,当我插入新记录时,审计工作完全正常。但是当我更新我的记录时,我的createdby和createddate设置为null。有什么方法可以避免修改这些列吗? 注意:column(可更新= false)特性不支持spring data jdbc。 我的审计实体如下所示: 以及使用@EnableJdbcAuditing和定义下面的

  • 我使用的是spring boot,因此我没有使用任何xml文件进行配置。我要做的是,在使用MongoRepositories保存数据时,启用OngOAuditing以保存createdDate、lastModifiedDate等。 我的模特课 我使用@CreateDate注释来保存createDate。我对DateTime使用了jodatime依赖项 Spring-data-mongoDB也添加在

  • 我正在使用Spring Mongo审计和@CreatedDate@CreatedBy不工作,但@LastModifiedDate和@LastModifiedBy工作正常。 我在配置类上添加了@EnableMongoAudting,并定义了AuditAware。 审核类别为: 当我保存文档时,它在createdOn和createdBy中都设置为null,但在modifiedOn和modifiedBy

  • 我有一个MongoDb数据库,其中包含一个名为的集合。在中,我希望通过属性搜索特定对象。我想更新找到的对象中的数组内容。例如,最初,在向该endpoint发出请求之前,所需的数据如下所示: 发出类似的请求后,mongodb中的数据应更新为 这是我的文件,为了执行它,注释中的代码应该是什么?

  • 问题内容: 某些数据库功能(例如和)很容易受到死锁的影响,因为数据库未指定将使用哪种锁定顺序。我发现有两次 讨论暗示此行为不是SQL标准指定的,更不用说具体的实现了。因此,我在假设我们无法控制锁定顺序的情况下进行操作(至少,这样做并不明显)。 如果我们不能依赖锁定顺序,应该如何避免数据库死锁? 如果我们不应该避免僵局(您将不得不非常努力地说服我),那么我们应该怎么做? 这个问题与数据库无关,所以请

  • 例如,我可以运行这个mongo查询: 我们如何用代码和spring MongoTemplate来实现这一点呢?例如,下面是UPDATE一个值的代码: Update#set方法似乎只接受一个(key、value),而不接受多值或值列表。