首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >Spring数据MongoDB:如何避免@CreatedBy和@CreatedDate字段更新?

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

Stack Overflow用户
提问于 2021-07-19 13:52:35
回答 2查看 2.3K关注 0票数 2

我使用Spring-Boot2.5.0和MongoDB来持久化一些文档。这里是Github项目

对于每个文档,我还需要自动保存一些审计信息,因此我扩展了以下类:

代码语言:javascript
运行
复制
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类:

代码语言:javascript
运行
复制
@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。

在这里他们用

代码语言:javascript
运行
复制
@Column(name = "created_by", updatable = false)

若要保护字段免遭更新,请执行以下操作。不幸的是,@Field for MongoDB没有这样的属性。

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

更新

我试图通过在一个doUpdate子类中重写MongoTemplate方法来实现这种行为。

代码语言:javascript
运行
复制
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方法。

代码语言:javascript
运行
复制
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

代码语言:javascript
运行
复制
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调用此方法,其中服务是上面定义的方法:

代码语言:javascript
运行
复制
default T save(T entity) {
    return this.convert(this.getService().save(this.decode(entity)));
}

这些是转换和解码方法:

代码语言:javascript
运行
复制
    @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:

代码语言:javascript
运行
复制
@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的保存方法,我创建了一些测试。

我发现的是:

  1. 保存方法第一次正确地创建了图书,所有的审计视界(version, creator, created, modifier, modified)都按预期填充。
  2. 保存方法正确地更新DB中的现有书籍,为后续的查找查询保留creatorcreated字段的现有值。
  3. save方法返回一个对象,并将创建的字段设置为null (但在DB中,文档中填充了所有审计字段)。

下面是我的测试方法(也可以在GitHub上使用)。

代码语言:javascript
运行
复制
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()方法之后就有某种拦截器覆盖了这些字段(创建者,创建的)。

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2021-08-04 07:31:51

一种可能的解决办法或解决办法是:

  1. 扩展MongoTemplate
  2. 重写doUpdate方法
  3. 将存储库savefind方法组合起来,以更新并返回已更新的对象和正确填充的审核字段。这是必需的,因为出于某种原因,repository.save方法返回审核字段(creatorcreated)的null,即使它们随后正确地填充在DB中。

在这里,我们需要覆盖MongoTemplateMongoTemplate方法。

代码语言:javascript
运行
复制
@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);
    }
}

最后,我使用了一个服务,它调用存储库进行保存和查找操作。这是它实现的接口。

代码语言:javascript
运行
复制
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自动生成审核字段(版本、创建者、创建、修饰符、修改)。
  2. 通过API和DTO返回这些字段。
  3. saveupdate API端点使用相同的DTO接口。
  4. 忽略通过API来自外部的审计值。
  5. 永远不要将审核字段保存到DB中的空值或错误值。
  6. 在更新API调用后返回正确和更新的审核字段。
票数 0
EN

Stack Overflow用户

发布于 2021-07-21 12:15:22

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

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

例如,Java在枚举中使用SNAKE_CASE,但出于某种疯狂的原因,您需要在API上使用kebab-case

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

票数 -1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/68441685

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档