source

Spring Data JPA 저장소에서 제네릭 사용

manycodes 2023. 9. 8. 21:36
반응형

Spring Data JPA 저장소에서 제네릭 사용

데이터베이스에 지속해야 하는 간단한 개체 유형이 몇 가지 있습니다.저는 이 지속성을 관리하기 위해 Spring JPA를 사용하고 있습니다.각 개체 유형에 대해 다음을 빌드해야 합니다.

import org.springframework.data.jpa.repository.JpaRepository;

public interface FacilityRepository extends JpaRepository<Facility, Long> {
}


public interface FacilityService {
    public Facility create(Facility facility);
}

@Service
public class FacilityServiceImpl implements FacilityService {

    @Resource
    private FacilityRepository countryRepository;

    @Transactional
    public Facility create(Facility facility) {
        Facility created = facility;
        return facilityRepository.save(created);
    }
}

객체 유형별로 다중 클래스를 3개의 제네릭 기반 클래스로 대체할 수도 있어 보일러 플레이트 코딩을 많이 절약할 수 있다는 생각이 들었습니다.나는 그것을 어떻게 해야 할지 정확히 모르겠어요 그리고 사실 그것이 좋은 생각일까요?

우선, 우리가 여기에 바를 꽤 올리고 있다는 것을 알지만, 이것은 이미 Spring Data JPA의 도움 없이 작성해야 했던 것보다 훨씬 적은 코드입니다.

둘째, 서비스 클래스는 처음부터 필요 없다고 생각합니다. 저장소로 전화를 전달하기만 하면 됩니다.트랜잭션 내에서 서로 다른 리포지토리를 조정해야 하는 비즈니스 로직이 있거나 캡슐화할 다른 비즈니스 로직이 있는 경우에는 리포지토리 앞에서 서비스를 사용하는 것이 좋습니다.

일반적으로 말하면 당연히 다음과 같은 작업을 수행할 수 있습니다.

interface ProductRepository<T extends Product> extends CrudRepository<T, Long> {

    @Query("select p from #{#entityName} p where ?1 member of p.categories")
    Iterable<T> findByCategory(String category);

    Iterable<T> findByName(String name);
}

이렇게 하면 클라이언트 측에서 다음과 같이 저장소를 사용할 수 있습니다.

class MyClient {

  @Autowired
  public MyClient(ProductRepository<Car> carRepository, 
                  ProductRepository<Wine> wineRepository) { … }
}

예상대로 작동할 겁니다그러나 몇 가지 주의해야 할 사항이 있습니다.

도메인 클래스가 단일 테이블 상속을 사용하는 경우에만 작동합니다.부트스트랩 시간에 얻을 수 있는 도메인 클래스에 대한 유일한 정보는 다음과 같습니다.Product물건들.그래서 다음과 같은 방법에 대해서는.findAll() 심지어findByName(…)련는과이다다이h과te련는ls으로 시작합니다.select p from Product p where…로 생성될 수 입니다. . 는 이 할 입니다 입니다 할 는 이 Wine아니면Car 특정 유형 정보를 캡처하기 위해 전용 리포지토리 인터페이스를 만들지 않는 경우.

일반적으로 Aggregate root당 저장소 인터페이스를 생성하는 것이 좋습니다.즉, 모든 도메인 클래스 자체에 대한 레포가 없습니다.더욱 중요한 것은 저장소를 통한 서비스의 1:1 추상화도 전혀 중요하지 않다는 점입니다.서비스를 구축한다고 해서 모든 저장소에 하나씩 구축하는 것은 아닙니다(원숭이가 그렇게 할 수도 있고, 우리는 원숭이가 아닙니다. ;).서비스는 더 높은 수준의 API를 제공하고, 훨씬 더 많은 사용 사례 드라이브를 제공하며, 일반적으로 여러 저장소에 대한 호출을 조정합니다.

또한 리포지토리 위에 서비스를 구축하는 경우 일반적으로 클라이언트가 리포지토리 대신 서비스를 사용하도록 강제합니다(여기서 고전적인 예는 사용자 관리를 위한 서비스도 암호 생성 및 암호화를 트리거하는 경우입니다).따라서 개발자가 암호화 작업을 효과적으로 수행하기 때문에 직접 저장소를 사용하도록 하는 것이 결코 좋은 방법은 아닐 것입니다.따라서 일반적으로 종속성을 만들지 않을 도메인 개체를 유지할 수 있는 사용자를 선택해야 합니다.

요약

예, 일반 저장소를 구축하고 여러 도메인 유형과 함께 사용할 수 있지만 기술적으로 상당히 엄격한 제한이 있습니다.그러나 건축학적 관점에서 보면 위에서 설명한 시나리오가 뜨지도 않아야 합니다. 이는 어쨌든 디자인 냄새에 직면하게 된다는 것을 의미하기 때문입니다.

이것은 매우 가능한 일입니다!아마도 파티에 많이 늦었을 것입니다.하지만 이것은 미래에 누군가에게 분명히 도움이 될 것입니다.여기에 매력적으로 작용하는 완벽한 솔루션이 있습니다!

만들기BaseEntity엔티티에 대한 클래스는 다음과 같습니다.

@MappedSuperclass
public class AbstractBaseEntity implements Serializable{

    @Id @GeneratedValue
    private Long id;
    @Version
    private int version;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    public AbstractBaseEntity() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    // getters and setters      
}

DAO 지속성을 위한 일반 JPA Repository 인터페이스를 다음과 같이 만듭니다. NB.기억하세요.@NoRepositoryBeanJPA가 저장소에 대한 구현을 찾지 않도록 합니다!

@NoRepositoryBean
public interface AbstractBaseRepository<T extends AbstractBaseEntity, ID extends Serializable>
extends JpaRepository<T, ID>{
    
}

위의 기본 JPA 저장소를 사용하는 기본 서비스 클래스를 만듭니다.도메인의 다른 서비스 인터페이스는 다음과 같이 간단히 확장됩니다.

public interface AbstractBaseService<T extends AbstractBaseEntity, ID extends Serializable>{
    public abstract T save(T entity);
    public abstract List<T> findAll(); // you might want a generic Collection if u prefer

    public abstract Optional<T> findById(ID entityId);
    public abstract T update(T entity);
    public abstract T updateById(T entity, ID entityId);   
    public abstract void delete(T entity);
    public abstract void deleteById(ID entityId);
    // other methods u might need to be generic
    
}

그런 다음 기본 JPA 저장소에 대한 추상적 구현을 생성하고 기본 CRUD 방법도 다음과 같이 구현됩니다.

@Service
@Transactional
public abstract class AbstractBaseRepositoryImpl<T extends AbstractBaseEntity, ID extends Serializable>
        implements AbstractBaseService<T, ID>{
    
    private AbstractBaseRepository<T, ID> abstractBaseRepository;
    
    @Autowired
    public AbstractBaseRepositoryImpl(AbstractBaseRepository<T, ID> abstractBaseRepository) {
        this.abstractBaseRepository = abstractBaseRepository;
    }
    
    @Override
    public T save(T entity) {
        return (T) abstractBaseRepository.save(entity);
    }

    @Override
    public List<T> findAll() {
        return abstractBaseRepository.findAll();
    }

    @Override
    public Optional<T> findById(ID entityId) {
        return abstractBaseRepository.findById(entityId);
    }

    @Override
    public T update(T entity) {
        return (T) abstractBaseRepository.save(entity);
    }

    @Override
    public T updateById(T entity, ID entityId) {
        Optional<T> optional = abstractBaseRepository.findById(entityId);
        if(optional.isPresent()){
            return (T) abstractBaseRepository.save(entity);
        }else{
            return null;
        }
    }

    @Override
    public void delete(T entity) {
        abstractBaseRepository.delete(entity);
    }

    @Override
    public void deleteById(ID entityId) {
        abstractBaseRepository.deleteById(entityId);
    }

}

의 의 사용방법entity,service,repository,그리고.implementation:

여기 예는 다음과 같습니다.MyDomain독립체. 확장하는 엔티티 생성합니다 도메인을 확장하는 도메인 엔티티를 .AbstractBaseEntity NB.:: NB.ID,createdAt,updatedAt,version으로 , 이 될 입니다 으로에 포함됩니다.MyDomain부터의 AbstractBaseEntity

@Entity
public class MyDomain extends AbstractBaseEntity{

    private String attribute1;
    private String attribute2;
    // getters and setters
}

그런 다음 생성합니다.repository을 위하여MyDomain을 하는 실체AbstractBaseRepository다음과 같이

@Repository
public interface MyDomainRepository extends AbstractBaseRepository<MyDomain, Long>{

}

serviceMyDomain엔티티는 다음과 같습니다.

public interface MyDomainService extends AbstractBaseService<MyDomain, Long>{

}

을 합니다 을 합니다 에 대한 구현을 제공합니다.MyDomain을 하는 실체AbstractBaseRepositoryImpl다음과 같이 구현합니다.

@Service
@Transactional
public class MyDomainServiceImpl extends AbstractBaseRepositoryImpl<MyDomain, Long> 
        implements MyDomainService{
    private MyDomainRepository myDomainRepository;

    public MyDomainServiceImpl(MyDomainRepository myDomainRepository) {
        super(myDomainRepository);
    }
    // other specialized methods from the MyDomainService interface

}
Now use your `MyDomainService` service in your controller as follows: 

@RestController // or @Controller
@CrossOrigin
@RequestMapping(value = "/")
public class MyDomainController {
    
    private final MyDomainService myDomainService;

    @Autowired
    public MyDomainController(MyDomainService myDomainService) {
        this.myDomainService = myDomainService;
    }
   
    @GetMapping
    public List<MyDomain> getMyDomains(){
        return myDomainService.findAll();
    }   
    // other controller methods

}

NB. 반드시.AbstractBaseRepository이다로 이 달렸습니다.@NoRepositoryBeanJPA콩에 대한 구현을 찾으려 하지 않습니다.도.AbstractBaseServiceImpl추상적으로 표시되어야 합니다. 그렇지 않으면 JPA는 모든 어린이 다오를 자동 배선하려고 할 것입니다.AbstractBaseRepositoryA로 이어지는 클래스의 생성자에서.NoUniqueBeanDefinitionException콩이 생성되면 1개 이상의 다오(수납고)가 주입되기 때문에!이제 당신의service,repository,그리고.implementations재사용이 더 가능합니다.우리는 모두 보일러 플레이트를 싫어합니다!

누군가에게 도움이 되길 바랍니다.

@Jose Mlanga의 답변에서 영감을 얻어 한 개의 수업을 덜 사용하여 이를 수행할 수 있는 또 다른 방법을 찾았습니다.

만들기BaseEntity

@Getter
@Setter
@MappedSuperclass
public class BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
}

만들기BaseRepository

@NoRepositoryBean
public interface BaseRepository<T extends BaseEntity> extends JpaRepository<T, Long> { }

마지막으로 생성합니다.BaseService설정을 완료하다

public interface BaseService<T extends BaseEntity> {

    BaseRepository<T> getRepository();

    default T create(T t) {
        return getRepository().save(t);
    }
    
    default Optional<T> update(T t) {
        if (getRepository().existsById(t.getId())) {
            return Optional.of(getRepository().save(t));
        }
        return Optional.empty();
    }
    
    default Optional<T> get(Long id) {
        return getRepository().findById(id);
    }
    
    default List<T> getAll() {
        return getRepository().findAll();
    }
    
    default void delete(Long id) {
        getRepository().deleteById(id);
    }
    
}

이제 우리는 우리의 엔티티를 만들기 시작할 수 있습니다.예를 들어, 우리가 우리에게Category. 우리는 모델, 저장소, 서비스, 컨트롤러를 만들 것입니다.

엔티티 및 저장소는 다음과 같이 나타납니다.

@Getter
@Setter
@Entity
public class Category extends BaseEntity { String name; }

public interface CategoryRepository extends BaseRepository<Category> { }

서비스를 위해서는 한 가지 방법만 재정의해야 합니다.getRepository()

@ApplicationScoped
public class CategoryService implements BaseService<Category> {

    @Inject
    CategoryRepository categoryRepository;

    @Override
    public BaseRepository<Category> getRepository() {
        return categoryRepository;
    }

}

마지막으로 컨트롤러를 만듭니다.컨트롤러에 대한 추상화를 만들 수 있는 범위에 대해서도 아직 도달하지 못했습니다.수정하면 수정하겠습니다.

@Path("categories")
@ApplicationScoped
public class CategoryController {

    // You can (and should!) use BaseService here.
    // Omitting that part as that would need qualifier.
    @Inject
    CategoryService categoryService;

    @POST
    public Response create(Category category) {
        return Response.status(Status.CREATED).entity(categoryService.create(category)).build();
    }

    @PUT
    @Path("{id}")
    public Response update(Category category, @PathParam("id") Long id) {
        if (Objects.isNull(category.getId()))
            category.setId(id);
        return categoryService.update(category).map(i -> Response.status(Status.ACCEPTED).entity(i).build())
                .orElseGet(() -> Response.status(Status.NOT_FOUND).build());
    }
    
    @DELETE
    @Path("{id}")
    public Response delete(@PathParam("id") Long id) {
        categoryService.delete(id);
        return Response.status(Status.ACCEPTED).build();
    }
    
    @GET
    @Path("{id}")
    public Response get(@PathParam("id") Long id) {
        return categoryService.get(id).map(i -> Response.status(Status.OK).entity(i).build())
                .orElseGet(() -> Response.status(Status.NO_CONTENT).build());
    }
    
    @GET
    public Response get() {
        return Response.status(Status.OK).entity(categoryService.getAll()).build();
    }
}

도움이 되길 바랍니다.건배!

저는 스프링 데이터로 카산드라의 일반 저장소를 만드는 프로젝트를 진행하고 있습니다.

먼저 코드가 있는 저장소 인터페이스를 만듭니다.

StringBuilder sourceCode = new StringBuilder();
sourceCode.append("import org.springframework.boot.autoconfigure.security.SecurityProperties.User;\n");
sourceCode.append("import org.springframework.data.cassandra.repository.AllowFiltering;\n");
sourceCode.append("import org.springframework.data.cassandra.repository.Query;\n");
sourceCode.append("import org.springframework.data.repository.CrudRepository;\n");
sourceCode.append("\n");
sourceCode.append("public interface TestRepository extends CrudRepository<Entity, Long> {\n");
sourceCode.append("}");

코드를 컴파일해서 클래스를 얻으면 org.mdkt.compiler를 사용합니다.In Memory Java Compiler

ClassLoader classLoader = org.springframework.util.ClassUtils.getDefaultClassLoader();
compiler = InMemoryJavaCompiler.newInstance();
compiler.useParentClassLoader(classLoader);
Class<?> testRepository = compiler.compile("TestRepository", sourceCode.toString());

그리고 저장소를 스프링 데이터 런타임에 초기화합니다.스프링에서 저장소 인터페이스를 초기화하는 방법을 찾기 위해 SpringData 코드를 디버그하기 때문에 이것은 조금 어렵습니다.

CassandraSessionFactoryBean bean = context.getBean(CassandraSessionFactoryBean.class);
RepositoryFragments repositoryFragmentsToUse = (RepositoryFragments) Optional.empty().orElseGet(RepositoryFragments::empty); 
CassandraRepositoryFactory factory = new CassandraRepositoryFactory(
    new CassandraAdminTemplate(bean.getObject(), bean.getConverter()));
factory.setBeanClassLoader(compiler.getClassloader());
Object repository = factory.getRepository(testRepository, repositoryFragmentsToUse);

이제 저장소의 저장 방법을 시도할 수 있고 findById와 같은 다른 방법을 시도할 수 있습니다.

Method method = repository.getClass().getMethod("save", paramTypes);
T obj = (T) method.invoke(repository, params.toArray());

제가 이 보고서 https://github.com/maye-msft/generic-repository-springdata 에 입력한 전체 샘플 코드와 구현 방법입니다.

비슷한 논리로 JPA로 확장할 수 있습니다.

언급URL : https://stackoverflow.com/questions/19417670/using-generics-in-spring-data-jpa-repositories

반응형