Active Record (opens new window) 模式
在软件工程中,主动记录模式(active record pattern)是一种架构模式, 可见于在关系数据库中存储内存中对象的软件中。 它在Martin Fowler的2003年著《企业应用架构的模式》书中命名。 符合这个模式的对象的接口将包括函数比如插入、更新和删除, 加上直接对应于在底层数据库表格中列的或多或少的属性。
主动记录模式是访问在数据库中的数据的一种方式。数据库表或视图被包装入类。 因此,对象实例被链接到这个表格的一个单一行。 在一个对象创建之后,在保存时将一个新行增加到表格中。 加载的任何对象都从数据库得到它的信息。在一个对象被更新的时候, 在表格中对应的行也被更新。包装类为在表格或视图中的每个列都实现访问器方法或属性。
activerecord 模块提供了 4 个接口类、1个工具类、1个配置类和1个Spring Boot自动配置类。
下面分别说明每个类的方法和设计的目的。
# 6.1 MapperRecord
这个接口定义如下:
public interface MapperRecord<T, I extends Serializable, M extends Mapper<T, I>> extends EntityInfoMapper<T> {
/**
* 通用 Mapper
*
* @return 通用 Mapper
*/
default M baseMapper() {
return MapperProvider.<T, I, M>getDefaultInstance().baseMapper(entityClass());
}
}
可以看到这个接口还继承了一个 EntityInfoMapper<T>
接口,继承的这个接口可以根据泛型 <T>
获取 T
对应的具体的实体类类型。
也就是这里默认方法中的 entityClass()
方法,当前接口中定义的这个默认方法用于返回实体类对应的 Mapper
接口,
获取接口的方式为通过 MapperProvider.<T, I, M>getDefaultInstance()
得到一个默认实现,然后根据当前实体类类型,
从默认实现中得到一个 Mapper
实例。
在实体类中得到自己对应的 Mapper
后就能调用各种通用的方法,下面要看的两个接口就是对该默认方法 baseMapper()
的应用。
# 6.2 EntityRecord
EntityRecord
接口中包含了实体类的基础 CRUD 方法,接口中全是默认方法,
实体类继承就可以非常直接进行增删改查操作,下面是方法列表和介绍:
default void save()
:保存(所有字段)default void saveSelective()
:保存(非空字段,空的字段会使用数据库设置的默认值,但是不会字段反写)default void update()
:根据主键更新(所有字段)default void updateSelective()
:根据主键更新(非空字段)default void updateSelective(Fn<T, Object>... forceUpdateFields)
:根据主键更新(非空字段),指定的强制更新字段不区分是否为空default I pkValue()
:返回主键值,建议子类替换为效率更高的判断方式(例如主键为 id 的情况下,直接 return id)default boolean pkHasValue()
:主键是否有值default void saveOrUpdate()
:保存或更新(全部字段),当主键不存在时保存,存在时更新default void saveOrUpdateSelective()
:保存或更新(非空字段),当主键不存在时保存,存在时更新default int delete()
:根据当前类的值作为条件进行删除(注意:当所有字段都没有值时可能会清库)default void deleteById()
:根据主键进行删除default void deleteById(I id)
:根据指定的主键进行删除default <F> int deleteByFieldList(Fn<T, F> field, List<F> fieldValueList)
:根据指定字段集合删除default T findById(I id)
:根据指定的主键查询default T findOne()
:以当前类作为条件查询一个,当结果多于1个时出错default List<T> findList()
:以当前类作为条件查询default <F> List<T> findByFieldList(Fn<T, F> field, List<F> fieldValueList)
:根据指定字段集合查询default List<T> findAll()
:查询全部default long count()
:以当前类作为条件查询总数
大部分方法都是以当前实体为参数进行增删改查的操作,少数几个方法可以通过传参进行其他的操作。 实际上除了以自己作为参数的方法外,其他外部参数的方法都应该设为静态方法才合适, 由于当前接口的形式无法实现,只能通过创建一个实例后再进行操作。
# 6.3 ExampleRecord
ExampleRecord
和上面的接口类似,提供了 Example
相关的方法:
default Example<T> example()
:获取 Example 对象default int delete(Example<T> example)
:根据 example 条件批量删除default int update(Example<T> example)
:根据 example 查询条件批量更新(所有字段)default int updateSelective(Example<T> example)
:根据 example 查询条件批量更新(非空字段)default T findOne(Example<T> example)
:根据 example 条件查询一个,当结果多于1个时出错default List<T> findList(Example<T> example)
:根据 example 条件查询default long count(Example<T> example)
:根据 example 查询总数
# 6.4 ActiveRecord
ActiveRecord
没有定义方法,只是单独的把上面两个接口集合到一起:
/**
* 建议将继承该抽象类的实现类的作用范围控制在 Service 层,不能超出范围,其它层使用时转换为 VO 或 DTO 后使用
*
* @param <T> 实体类类型
* @param <I> 主键类型
* @author liuzh
*/
public interface ActiveRecord<T, I extends Serializable>
extends EntityRecord<T, I>, ExampleRecord<T, I> {
}
重点看一下上面的注释,说一说 ActiveRecord 模式的应用场景。
在一个简单的增删改查系统中,如果用不到 service 层,那就可以直接在 controller 层用实体类进行直接的CRUD。
但是在一个有 service 层的系统中,如果要实现模块的隔离,就需要考虑实体类只能在 service 层使用, 不能让实体类逸出到其他层,因此在和 controller 层交互时就需要有 VO 或 DTO 进行数据转换, 这样才能保证实体类调用 CRUD 方法不出错。
# 6.5 MapperProvider
第一个接口中就调用到了当前类的静态方法,当前类中提供了两个静态方法:
/**
* 获取默认实例
*
* @return 默认实例
*/
public static <T, I extends Serializable, M extends Mapper<T, I>> MapperProvider<T, I, M> getDefaultInstance() {
return MapperProviderInstance.getINSTANCE();
}
/**
* 获取指定的实例
*
* @return 默认实例
*/
public static <T, I extends Serializable, M extends Mapper<T, I>> MapperProvider<T, I, M> getInstance(String instanceName) {
return (MapperProvider<T, I, M>) applicationContext.getBean(instanceName);
}
当系统只有一个数据源时,直接用第一个方法返回默认数据源即可,当系统存在多个数据源时,就需要第二个方法来指定调用哪一个数据源。
当前模块的测试用例就是一个多数据源的,我们看看如何配置的,这里使用了 Spring XML 形式, 主要部分的两个配置如下
<!-- 通过 init-method 将当前实例作为默认实现 -->
<bean id="mapperProviderRole" class="io.mybatis.activerecord.spring.MapperProvider"
init-method="registerAsDefault">
<constructor-arg index="0" ref="sqlSessionTemplateRole"/>
</bean>
<bean id="mapperProviderUser" class="io.mybatis.activerecord.spring.MapperProvider">
<constructor-arg index="0" ref="sqlSessionTemplateUser"/>
</bean>
这里配置了两个 MapperProvider
实现,第一个方法在执行了 registerAsDefault
,这个方法定义如下:
/**
* 将当前实例设置为默认实例
*/
public void registerAsDefault() {
MapperProviderInstance.setINSTANCE(this);
}
这个方法将自己设为了默认实例。另一个 mapperProviderUser
不是默认的,这个对象在下面的基类中使用:
public class BaseId<T extends BaseId> implements ActiveRecord<T, Integer> {
@Entity.Column(id = true, insertable = false)
private Integer id;
@Override
public Integer pkValue() {
return id;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public Mapper<T, Integer> baseMapper() {
//获取指定数据源的实例
return MapperProvider.<T, Integer, Mapper<T, Integer>>getInstance("mapperProviderUser").baseMapper(entityClass());
}
}
这个基类重新了 baseMapper()
使用了指定数据源创建的 mapperProviderUser
。
这个类中还有一个希望实现的最佳实践方法,就是 pkValue()
方法,这个方法的默认实现如下:
/**
* 返回主键值,建议子类替换为效率更高的判断方式(例如主键为 id 的情况下,直接 return id)
*
* @return 主键值
*/
default I pkValue() {
EntityTable entityTable = baseMapper().entityTable();
List<EntityColumn> idColumns = entityTable.idColumns();
if (idColumns.size() == 1) {
return (I) idColumns.get(0).field().get(this);
} else {
return idColumns.get(0).field().get(this) != null ? (I) this : null;
}
}
可以看到这里先得到了当前实体对应的主键列,然后通过反射的方式判断主键字段是否有值。
虽然这个方法的执行效率没太大影响,但是如果重新成下面的方法,肯定更高效:
@Override
public Integer pkValue() {
return id;
}
在这里的 Spring 示例中,通过 XML 定义的 MapperProvider
,当使用 Spring Boot 时,可以自动配置直接使用。
# 6.6 MapperProviderAutoConfiguration
MapperProviderAutoConfiguration
用来自动配置 MapperProvider
,配合使用的 MapperProviderProperties
目前没太大用途,
只是可以用来配置关闭自动配置。
当应用中没有 MapperProvider
实例时,会自动创建一个。当存在一个实例时,还会自动执行实例的 registerAsDefault()
方法注册为默认实例。
当 Spring Boot 中也存在多数据源时,可以通过给其中一个实例添加 @Primary
来指定首选的默认实例。