译见|构建用户管理微服务(三):实现和测试存储库

320 阅读5分钟

在上期的《译见|构建用户管理微服务(二):实现领域模型 》中,有着相当数量的涉及到实现领域模型的编码,它们构成了用户注册过程中所需要的全部逻辑。在第三部分中,作者将带你继续前行:详细介绍一个完整的基于 JPA 的用户存储库实现,一个 JPA 的支撑模型和一些测试用例。

使用 XML 来映射简单的 JAVA 对象


仅看到用户存储库,也许你就能想到在对它添加基于 JPA 的实现时会遇到什么困难。


public interface UserRepository {  

void delete(Long userId) throws NoSuchUserException;  Optional<User> findById(Long id);  Optional<User> findByEmail(String email);  Optional<User> findByScreenName(String screenName);  User save(User user); }


但是, 正如我在第一部分提到的, 我们将使用 DDD (域驱动设计), 因此, 在模型中就不能使用特定框架的依赖关系云 (包括 JPA 的注解) ,剩下的唯一可行性方法是用 XML 进行映射。如果我没有记错的话,自2010年以来,我再也没有接触过任何一个 orm.xml 的文件 , 这也就是我为什么开始怀念它的原因。

接下来我们看看XML文件中User的映射情况,以下是 user-orm.xml 的部分摘录。

<entity class="com.springuni.auth.domain.model.user.User" cacheable="true" metadata-complete="true" >
<table name= "user_"/>
<named-query name="findByIdQuery" >  <query>    <![CDATA[
     select u from User u      
     where u.id = :userId      
     and u.deleted = false    
   ]]>  </query>
</named-query>
<named-query name="findByEmailQuery" >  <query>    <![CDATA[      
     select u from User u      
     where u.contactData.email = :email      
     and u.deleted = false    
   ]]>  </query>
</named-query>
<named-query name= "findByScreenNameQuery">  <query>    <![CDATA[      
     select u from User u      
     where u.screenName = :screenName      
     and u.deleted = false    
   ]]>  </query>
</named-query>
<entity-listeners>  <entity-listener class= "com.springuni.commons.jpa.IdentityGeneratorListener"/>
</entity-listeners>
<attributes>  <id name= "id"/>  <basic name= "timezone">    <enumerated> STRING</enumerated>  </basic>  <basic name= "locale"/>  <basic name= "confirmed"/>  <basic name= "locked"/>  <basic name= "deleted"/>  <one-to-many name= "confirmationTokens" fetch="LAZY" mapped-by= "owner" orphan-removal="true" >    <cascade>      <cascade-persist/>      <cascade-merge/>    </cascade>  </one-to-many>  <element-collection name="authorities" >    <collection-table name="authority" >       <join-column name= "user_id" />     </collection-table>   </element-collection>   <embedded name= "auditData" />   <embedded name= "contactData" />   <embedded name= "password" />   <!-- Do not map email directly through its getter/setter -->   <transient name= "email" />

</attributes>

</entity>


域驱动设计是一种持久化无关的方法,因此坚持设计一个没有具体目标数据结构的模型可能很有挑战性。当然, 它也存在优势, 即可对现实世界中的问题直接进行建模, 而不存在只能以某种方式使用某种技术栈之类的副作用。

public class User implements Entity<Long, User> {  

private Long id;  
private String screenName;  ...  

private Set<String> authorities = new LinkedHashSet<>();

}

一般来说,一组简单的字符串或枚举值就能对用户的权限(或特权)进行建模了。


使用像 MongoDB 这样的文档数据库能够轻松自然地维护这个模型,如下所示。(顺便一提, 我还计划在本系列的后续内容中添加一个基于 Mongo 的存储库实现)


{   "id" :123456789,  
"screenName": "test",   ...  
"authorities" :[      
              "USER" ,      
              "ADMIN"   ] }

然而, 在关系模型中, 权限的概念必须作为用户的子关系进行处理。但是在现实世界中, 这仅仅只是一套权限规则。我们需要如何弥合这样的差距呢?

在 JPA 2.0 中可以引入 ElementCollection 来进行操作,它的用法类似于 OneToMany。在这种情况下, 已经配置好的 JPA 提供的程序 (Hibernate) 将自动生成必要的子关系。

create table authority (  user_id bigint not null,  authorities varchar(255) )

alter table authority  add constraint FKoia3663r5o44m6knaplucgsxn  foreign key (user_id) references user_


项目中的新模块


我一直在讨论的 springuni-auth-user-jpa 包含了一个完整的基于 JPA 的 UserRepository 实现。其目标是, 每个模块都应该只拥有那些对它们的操作来说绝对必要的依赖关系,而这些关系只需要依赖 JPA API 便可以实现。

springuni-commons-jpa 是一个支撑模块, 它能够使用预先配置好的 HikariCP 和 Hibernate 的组合作为实体管理器, 而不必关心其他细节。 它的特色是 AbstractJpaConfiguration, 类似于 Spring Boot 的 HibernateJpaAutoConfiguration。

然而我没有使用后者的原因是 Spring Boot 的自动配置需要一定的初始化。因为谷歌应用引擎标准环境是我的目标平台之一,因此能否快速地启动是至关重要的。

单元测试存储库


虽然有人可能会说, 对于存储库没必要进行过多的测试, 尤其是在使用 Spring Data 的 存储库接口的时候。但是我认为测试代码可以避免运行时存在的一些问题,例如错误的实体映射或错误的 JPQL 查询。

@RunWith(SpringJUnit4ClassRunner) @ContextConfiguration(classes = [UserJpaTestConfiguration]) @Transactional @Rollbackclass UserJpaRepositoryTest {  @Autowired  UserRepository userRepository  User user  @Before  void before() {    user = new User( 1, "test" , "test@springuni.com")    user.addConfirmationToken(ConfirmationTokenType. EMAIL, 10 )    userRepository.save (user)  }  ...  @Test  void testFindById() {    Optional<User> userOptional = userRepository.findById (user.id)    assertTrue(userOptional.isPresent())  }  ... }

这个测试用例启动了一个具有嵌入式 H2 数据库的实体管理器。H2 非常适合于测试, 因为它支持许多众所周知的数据库 (如 MySQL) 的兼容模式,可以模拟你的真实数据库。

下期预告:构建用户管理微服务(四):实现 REST 控制器

原文链接:https://www.springuni.com/user-management-microservice-part-3



☟ 点击查看 上期文章