【问题标题】:Pattern to implement generic CRUD Controller实现通用 CRUD 控制器的模式
【发布时间】:2019-09-09 16:32:52
【问题描述】:

我有一堆表,我需要为其提供标准的 CRUD 接口。每次我必须公开一个新表时,我都会遵循以下模式。

public interface EntityWithId<TDbEntity> extends Serializable {
    public TDbEntity entityId();
}

@Entity
public class DbEntityName implements EntityWithId<Long> {
    @Id private Long id;
    @Override public Long entityId() {return id;}

    // other fields follow
}

public class EntityName {
    private Long id;

    // other fields follow

    // create Entity from DbEntity
    public EntityName(DbEntityName dbItem) { ... }

    // get DbEntity from Entity
    public DbEntityName toDb() { ... }
}

@Repository
public interface DbEntityNameRepository extends CrudRepository<DbEntityName, Long> { }

public interface CrudService<TDbEntity extends EntityWithId<ID>, ID> {
    CrudRepository<TDbEntity, ID> getCrudRepository();

    // provide default implementation of all CRUD operations here like the below one
    default TDbEntity save(TDbEntity entity) { return getCrudRepository().save(entity); }
}


public interface DbEntityNameService extends CrudService<DbEntityName, Long> {
}

@Service
public class DbEntityNameServiceImpl implements DbEntityNameService {
    @lombok.Getter @Autowired DbEntityNameRepository crudRepository;
}

@RestController
@RequestMapping("/api/v1/dbservice")
public class EntityNameController {

    @Autowired DbEntityNameService dbService;

    @PostMapping("/{EntityName}") // this should be replaced by the actual name of the entity
    public Long save(@RequestBody EntityName msg) {
        return dbService.save(msg.toDb()).entityId();
    }

    // implement other CRUD end points
}

EntityWithId&lt;T&gt; 接口和CrudService&lt;TDbEntity extends EntityWithId&lt;ID&gt;, ID&gt; 只为系统定义一次。它们提供了在访问 repo 时摆脱重复代码的机制。

您会注意到,唯一需要完成的真正代码是添加实体和数据库实体中的字段,以及它们的转换。另外,我需要为每个新表推出一个新的控制器。

问题:我如何构造控制器代码,以便我可以从基本 CRUD 控制器继承功能。

请注意,在我的真实代码中,并非所有实体都用于简单的 CRUD,并且当前结构提供了扩展服务的简便方法

简而言之,我正在寻找一些模式来帮助我提供如下所示的内容,其中我有一个通用的 Base 类,我可以用最少的代码创建一个子类来公开控制器的端点。不用说,以下代码无法按原样提供我正在寻找的功能。

class BaseController<TEntity, TDbEntity, TId> {
    CrudService<TDbEntity, TId> dbService;

    @GetMapping("/{TEntity}/{id}")
    public TEntity getById(@PathVariable TId id) { 
        return new TEntity(dbService.getById(id));
    }

    @PostMapping("/{TEntity}")
    public Long save(@RequestBody TEntity msg) { 
        return dbService.save(msg.toDb()).entityId();
    }
}

class EntityNameController : BaseController<EntityName, DbEntityName, Long> {
}

也欢迎提供其他建议。我的目的是减少控制器中的重复代码 - 这主要是创建 CRUD 函数,将其与 CRUD 端点相关联并调用底层服务来完成实际工作。

编辑:我知道我可以编写一个自定义注释处理器来生成标准的 CRUD 函数(几乎就像CrudRepository 的工作方式一样),但这不是我想要的方向。

澄清一下,这里的目的是标准功能(如 CRUD)可以在将公开它的基本控制器中一劳永逸地编码,从而释放子控制器来处理其他非标准工作。

【问题讨论】:

  • @PaulPearson 这就是我想要弄清楚的。这样的代码会是什么样子? dbService 将如何被初始化。 GetMapping 和 PostMapping '继承'等将如何?
  • Tbh 这听起来你应该使用 Spring Data Rest。基本上,您将让您的存储库公开所有标准端点,如果需要,您可以为非标准端点添加指定的 RepositoryControllers。
  • @MirkoBrandt 感谢您的建议。我正在阅读它,它看起来像我可能正在寻找的东西。尝试查找是否可以 1. 在不破坏已经公开的 API(控制输出 JSON 和 URL 端点)的情况下合并它。2。在控制器中添加新的自定义处理程序/端点(不在 repo 中)。
  • @MirkoBrandt 不幸的是,我不知道如何在当前情况下使用 Spring Data Rest。尽管 Spring Data Rest 提供了一种方法来做我所要求的,但它并不能让我很好地控制 JSON 返回。此外,它不完全支持 api 模型和 db 模型的概念(我们可以使用 Jackson 序列化器/反序列化器破解它)。该机制绕过服务层,并在控制器中公开 repo(这很棒),但在我的情况下,我在服务层中进行了一堆验证、解析、查找等

标签: java spring rest jpa


【解决方案1】:

我认为对于 dbService,您可以使用类似的东西

public interface CrudService<T, ID> {

    T findByName(String name);

    Set<T> findAll();

    T findById(ID id);

    T save(T object);

    void delete(T object);

    void deleteById(ID id);

}

public interface EntityNameService extends CrudService<EntityName, Long> {
}


public class EntityNameServiceImpl implements EntityNameService {

    @Inject
    private DbEntityNameRepository repository;

    // implement all your repo code here
}

你的基本控制器可以像这样开始

public class BaseController {

    @Autowired
    private EntityNameService service;

    public String getEntityName(String name) {
        service.findByName(name);
    }

    @PostMapping("/{EntityName}") // this should be replaced by the actual name of the entity
    public Long save(@PathVariable String EntityName) {
        getEntityName(EntityName);
        return dbService.save(msg.toDb()).entityId();
    }

}

【讨论】:

  • 你能开发更多代码来展示真正的控制器是怎样的吗?我已经在我的问题中知道了您在答案中提出的内容。
  • 如何避免在子类中指定PostMapping?我希望基类提供功能和映射。
  • 我的目的是减少控制器中的“重复”代码模式,我最终将为每个表创建一个。到目前为止,上述方法并未提出任何会减少代码的建议。对所有表使用一个控制器也不会减少代码,而是继续隐藏重复的代码模式
  • 您的 CrudService 看起来非常接近 Spring Data JpaRepository:spring.io/blog/2011/02/10/getting-started-with-spring-data-jpa
【解决方案2】:

这是摆脱一些样板的尝试。这个想法是业务逻辑将位于服务中,而不是在 RestController 或 Repository 中。 服务可以重用并且可以很好地进行单元测试。

QueryDSL 和 SpringData 是你的朋友:

<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
</dependency>

一个基础仓库。

import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;

@NoRepositoryBean
public interface BaseRepo<T , ID > extends PagingAndSortingRepository<T, ID>, QuerydslPredicateExecutor<T> {}

可以访问超大表的真实存储库:

import com.querydsl.core.types.Predicate;
import static java.lang.System.out;
import refactor.BaseRepo;

public interface MyEntityRepository extends BaseRepo<MyEntity, String> {
    @Override
    default long count(Predicate predicate){
        //counts on very large tables take forever. Optionally add this
        return 0;
    }
    @Override
    default long count(){
        //counts on very large tables take forever. Optionally add this
        return 0;
    }    
}

基础服务:

import com.querydsl.core.types.Predicate;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

@RequiredArgsConstructor
public class BaseService<T, ID> {

    final BaseRepo<T, ID> repo;

    public Page<T> findAll(Predicate predicate, Pageable pageable) {
        return repo.findAll(predicate, pageable);
    }

    public Iterable<T> findAllWithoutMeta(Predicate predicate, Pageable pageable) {
        return repo.findAll(predicate, pageable);
    }

    public Iterable<T> findAll() {
        return repo.findAll();
    }

    public T save(T vendor) {
        return repo.save(vendor);
    }

    public T update(T vendor) {
        return repo.save(vendor);
    }

    public void delete(ID id) {
        repo.deleteById(id);
    }

    public boolean exists(ID id) {
        return repo.findById(id).isPresent();
    }

    public Optional<T> getById(ID id) {
        return repo.findById(id);
    }

}

真正的服务:

import com.querydsl.core.types.Predicate;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

@Service
public class MyService extends BaseService<MyEntity, String>{

    public MyService(MyEntityRepository repo) {
        super(repo);
    }

    @Override
    public Page<MyEntity> findAll(Predicate predicate, Pageable pageable) {
        return super.findAll(predicate, pageable); 
    }

}

我决定不对我的 RestContoller 进行改进,而只编写我需要的 CRUD 操作所需的代码。 (例如,在某些情况下不需要或不需要删除和放置操作) 这是一个 HATEOAS RESTful API 的实现。投资 HATEOAS 设计并不适合所有人和每个应用程序。这可以用一个普通的休息控制器代替。

get here 可以过滤存储库中的所有字段。这样你就可以得到http://localhost/api/v1/myapi?name=YourName&age=30

import com.querydsl.core.types.Predicate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Pageable;
import org.springframework.data.querydsl.binding.QuerydslPredicate;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(path = "/api/v1/myapi", produces = MediaTypes.HAL_JSON_VALUE)
public class MyApiController {

    private final MyService service;
    private final EntityLinks eLinks;

    MyApiController(MyService service, EntityLinks eLinks) {
        this.service = service;
        this.eLinks = eLinks;
    }

    @GetMapping
    @Transactional(readOnly = true)
    ResponseEntity<Resources<Resource<MyEntity>>> findAll(@QuerydslPredicate(root = MyEntity.class) Predicate predicate, Pageable pageable) {
        return new ResponseEntity(toResources(service.findAllWithoutMeta(predicate,pageable)), HttpStatus.OK);
    }

    @GetMapping(value = "/{id}")
    ResponseEntity<Resource<MyEntity>> findOne(@PathVariable String id) {
        final Optional<MyEntity> findById = service.getById(id);
        if (!findById.isPresent()) {
            return null;//fixme ResponseEntity.notFound(assembler.);
        }
        return ResponseEntity.ok(toResource(findById.get()));
    }

    private Resources<Resource<MyEntity>> toResources(Iterable<MyEntity> customers) {
        List<Resource<MyEntity>> customerResources = new ArrayList<>();
        for (MyEntity l : customers) {
            customerResources.add(toResource(l));
        }

        return new Resources<>(customerResources);//, selfLink);

    }

    private Resource<MyEntity> toResource(MyEntity customer) {
        Link selfLink = linkTo(methodOn(CallLoggingController.class).findOne(customer.getId())).withSelfRel();
        return new Resource<>(customer, selfLink);

    }

}

我的建议是不要沉迷于通用代码。复制和粘贴比超级通用代码更好。

【讨论】:

  • 谢谢@rjdkolb。我已经有一个非常相似的实现,其中我的存储库和服务被简化,没有任何重大重复。我只是无法理解如何减少 Controller 中“代码模式”的重复。不过,我同意您的观点,并且希望在必须公开新表时保持实际并减少“工作”。
猜你喜欢
  • 1970-01-01
  • 2015-01-17
  • 1970-01-01
  • 2015-09-30
  • 1970-01-01
  • 2015-04-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多