实现通用CRUD控制器的模式

时间:2019-09-09 16:32:52

标签: java spring rest jpa

我有一堆表,需要为其提供标准的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<T>接口和CrudService<TDbEntity extends EntityWithId<ID>, ID>仅为系统定义一次。它们提供了在访问存储库时消除重复代码的机制。

您将注意到,只需完成真正的代码即可在Entity和DB Entity中添加字段以及它们的转换。另外,我需要为每个新表推出一个新的Controller。

问题:如何构造控制器代码,以便可以从基本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)进行一次性编码,从而释放子控制器以处理其他非标准工作。

2 个答案:

答案 0 :(得分: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();
    }

}

答案 1 :(得分:0)

这是为了摆脱一些锅炉板的尝试。想法是,业务逻辑将位于服务中,而不位于RestController或存储库中。 服务可以被重用并且单元测试良好。

使用SpringData的QueryDSL是您的朋友:

<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设计并不适合每个人和每个应用程序。可以用一个普通的休息控制器代替。

获取此处信息可以过滤存储库中的所有字段。这样您就可以获得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);

    }

}

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