在上期的《译见|构建用户管理微服务(二):实现领域模型 》中,有着相当数量的涉及到实现领域模型的编码,它们构成了用户注册过程中所需要的全部逻辑。在第三部分中,作者将带你继续前行:详细介绍一个完整的基于 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
☟ 点击查看 上期文章