深入淺出Mybatis-sql自動生成
本文提供了一種自動生成sql語句的方法,它針對的對象是有主鍵或唯一索引的單表,提供的操作有增、刪、改、查4種。理解本文和本文的提供的代碼需要有java注解的知識,因為本文是基於注解生成sql的。本文適配的mybatis版本是3.2.2。
准備
為什么在StatementHandler攔截
在深入淺出MyBatis-Sqlsession章節介紹了一次sqlsession的完整執行過程,從中可以知道sql的解析是在StatementHandler里完成的,所以為了自動生成sql需要攔截StatementHandler。
MetaObject簡介
在我的實現里大量使用了MetaObject這個對象,因此有必要先介紹下它。MetaObject是Mybatis提供的一個的工具類,通過它包裝一個對象后可以獲取或設置該對象的原本不可訪問的屬性(比如那些私有屬性)。它有個三個重要方法經常用到:
1) MetaObject forObject(Object object,ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory)
2) Object getValue(String name)
3) void setValue(String name, Object value)
方法1)用於包裝對象;方法2)用於獲取屬性的值(支持OGNL的方法);方法3)用於設置屬性的值(支持OGNL的方法);
插件的原理
參見深入淺出Mybatis-插件原理。
有了上面這些基礎知識的准備后,就可以我們的主題了。
攔截器簽名
- @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})})
- public class AutoMapperInterceptor implements Interceptor {
- ...
- }
- @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})})
- public class AutoMapperInterceptor implements Interceptor {
- ...
- }
從簽名里可以看出,要攔截的目標類型是StatementHandler(注意:type只能配置成接口類型),攔截的方法是名稱為prepare參數為Connection類型的方法。
intercept的實現
- @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})})
- public class AutoMapperInterceptor implements Interceptor {
- private static final Log logger = LogFactory.getLog(AutoMapperInterceptor.class);
- private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
- private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
- @Override
- public Object intercept(Invocation invocation) throws Throwable {
- StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
- MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY,
- DEFAULT_OBJECT_WRAPPER_FACTORY);
- // 分離代理對象鏈
- while (metaStatementHandler.hasGetter("h")) {
- Object object = metaStatementHandler.getValue("h");
- metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY);
- }
- // 分離最后一個代理對象的目標類
- while (metaStatementHandler.hasGetter("target")) {
- Object object = metaStatementHandler.getValue("target");
- metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY);
- }
- String originalSql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
- Configuration configuration = (Configuration) metaStatementHandler.getValue("delegate.configuration");
- Object parameterObject = metaStatementHandler.getValue("delegate.boundSql.parameterObject");
- if (null == originalSql || "".equals(originalSql)) {
- String newSql = "";
- MappedStatement mappedStatement = (MappedStatement) metaStatementHandler
- .getValue("delegate.mappedStatement");
- // 根據ID生成相應類型的sql語句(id需剔除namespace信息)
- String id = mappedStatement.getId();
- id = id.substring(id.lastIndexOf(".") + 1);
- if ("insert".equals(id)) {
- newSql = SqlBuilder.buildInsertSql(parameterObject);
- } else if ("update".equals(id)) {
- newSql = SqlBuilder.buildUpdateSql(parameterObject);
- } else if ("delete".equals(id)) {
- newSql = SqlBuilder.buildDeleteSql(parameterObject);
- } else if ("select".equals(id)) {
- newSql = SqlBuilder.buildSelectSql(parameterObject);
- }
- logger.debug("Auto generated sql:" + newSql);
- //
- SqlSource sqlSource = buildSqlSource(configuration, newSql, parameterObject.getClass());
- List<ParameterMapping> parameterMappings = sqlSource.getBoundSql(parameterObject).getParameterMappings();
- metaStatementHandler.setValue("delegate.boundSql.sql", sqlSource.getBoundSql(parameterObject).getSql());
- metaStatementHandler.setValue("delegate.boundSql.parameterMappings", parameterMappings);
- }
- // 調用原始statementHandler的prepare方法
- statementHandler = (StatementHandler) metaStatementHandler.getOriginalObject();
- statementHandler.prepare((Connection) invocation.getArgs()[0]);
- // 傳遞給下一個攔截器處理
- return invocation.proceed();
- }
- @Override
- public Object plugin(Object target) {
- if (target instanceof StatementHandler) {
- return Plugin.wrap(target, this);
- } else {
- return target;
- }
- }
- @Override
- public void setProperties(Properties properties) {
- }
- private SqlSource buildSqlSource(Configuration configuration, String originalSql,
- Class<?> parameterType) {
- SqlSourceBuilder builder = new SqlSourceBuilder(configuration);
- return builder.parse(originalSql, parameterType, null);
- }
- }
- @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})})
- public class AutoMapperInterceptor implements Interceptor {
- private static final Log logger = LogFactory.getLog(AutoMapperInterceptor.class);
- private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
- private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
- @Override
- public Object intercept(Invocation invocation) throws Throwable {
- StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
- MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY,
- DEFAULT_OBJECT_WRAPPER_FACTORY);
- // 分離代理對象鏈
- while (metaStatementHandler.hasGetter("h")) {
- Object object = metaStatementHandler.getValue("h");
- metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY);
- }
- // 分離最后一個代理對象的目標類
- while (metaStatementHandler.hasGetter("target")) {
- Object object = metaStatementHandler.getValue("target");
- metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY);
- }
- String originalSql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
- Configuration configuration = (Configuration) metaStatementHandler.getValue("delegate.configuration");
- Object parameterObject = metaStatementHandler.getValue("delegate.boundSql.parameterObject");
- if (null == originalSql || "".equals(originalSql)) {
- String newSql = "";
- MappedStatement mappedStatement = (MappedStatement) metaStatementHandler
- .getValue("delegate.mappedStatement");
- // 根據ID生成相應類型的sql語句(id需剔除namespace信息)
- String id = mappedStatement.getId();
- id = id.substring(id.lastIndexOf(".") + 1);
- if ("insert".equals(id)) {
- newSql = SqlBuilder.buildInsertSql(parameterObject);
- } else if ("update".equals(id)) {
- newSql = SqlBuilder.buildUpdateSql(parameterObject);
- } else if ("delete".equals(id)) {
- newSql = SqlBuilder.buildDeleteSql(parameterObject);
- } else if ("select".equals(id)) {
- newSql = SqlBuilder.buildSelectSql(parameterObject);
- }
- logger.debug("Auto generated sql:" + newSql);
- //
- SqlSource sqlSource = buildSqlSource(configuration, newSql, parameterObject.getClass());
- List<ParameterMapping> parameterMappings = sqlSource.getBoundSql(parameterObject).getParameterMappings();
- metaStatementHandler.setValue("delegate.boundSql.sql", sqlSource.getBoundSql(parameterObject).getSql());
- metaStatementHandler.setValue("delegate.boundSql.parameterMappings", parameterMappings);
- }
- // 調用原始statementHandler的prepare方法
- statementHandler = (StatementHandler) metaStatementHandler.getOriginalObject();
- statementHandler.prepare((Connection) invocation.getArgs()[0]);
- // 傳遞給下一個攔截器處理
- return invocation.proceed();
- }
- @Override
- public Object plugin(Object target) {
- if (target instanceof StatementHandler) {
- return Plugin.wrap(target, this);
- } else {
- return target;
- }
- }
- @Override
- public void setProperties(Properties properties) {
- }
- private SqlSource buildSqlSource(Configuration configuration, String originalSql,
- Class<?> parameterType) {
- SqlSourceBuilder builder = new SqlSourceBuilder(configuration);
- return builder.parse(originalSql, parameterType, null);
- }
- }
StatementHandler的默認實現類是RoutingStatementHandler,因此攔截的實際對象是它。RoutingStatementHandler的主要功能是分發,它根據配置Statement類型創建真正執行數據庫操作的StatementHandler,並將其保存到delegate屬性里。由於delegate是一個私有屬性並且沒有提供訪問它的方法,因此需要借助MetaObject的幫忙。通過MetaObject的封裝后我們可以輕易的獲得想要的屬性。
在上面的方法里有個兩個循環,通過他們可以分離出原始的RoutingStatementHandler(而不是代理對象)。
有了插件幫你生成sql語句后,mapper配置文件里單表的增刪改查部分就不需要再配置sql代碼了,但由於插件需要通過id來生成不同類型的sql語句,因此必要的配置還是需要的,而且相應的id必須是下面的這幾個(區分大小寫):
- <update id="update" parameterType="UserDto"></update>
- <insert id="insert" parameterType="UserDto"></insert>
- <delete id="delete" parameterType="UserDto"></delete>
- <select id="select" parameterType="UserDto" resultType="UserDto""></select>
- <update id="update" parameterType="UserDto"></update>
- <insert id="insert" parameterType="UserDto"></insert>
- <delete id="delete" parameterType="UserDto"></delete>
- <select id="select" parameterType="UserDto" resultType="UserDto""></select>
SqlBuilder
SqlBuilder的相應方法接受一個dto對象作為參數,它們根據這個對象的屬性值和配置的注解生成相應的sql。
- @TableMapperAnnotation(tableName = "t_user", uniqueKeyType = UniqueKeyType.Single, uniqueKey = " userid ")
- public class UserDto {
- @FieldMapperAnnotation(dbFieldName = "userid", jdbcType = JdbcType.INTEGER)
- private Integer userid;
- @FieldMapperAnnotation(dbFieldName = "username", jdbcType = JdbcType.VARCHAR)
- private String username;
- ...
- }
- @TableMapperAnnotation(tableName = "t_user", uniqueKeyType = UniqueKeyType.Single, uniqueKey = " userid ")
- public class UserDto {
- @FieldMapperAnnotation(dbFieldName = "userid", jdbcType = JdbcType.INTEGER)
- private Integer userid;
- @FieldMapperAnnotation(dbFieldName = "username", jdbcType = JdbcType.VARCHAR)
- private String username;
- ...
- }
這個對象包含了兩種注解,一個是TableMapperAnnotation注解,它保存了表名、唯一鍵類型和構成唯一鍵的字段;另一個是FieldMapperAnnotation注解,它保存了數據庫字段名和字段類型信息。這兩個注解都是必須的。SqlBuilder生成sql時會用到他們,下面以生成insert語句的方法為例,其他方法類似:
- public static String buildInsertSql(Object object) throws Exception {
- if (null == object) {
- throw new RuntimeException("Sorry,I refuse to build sql for a null object!");
- }
- Map dtoFieldMap = PropertyUtils.describe(object);
- // 從參數對象里提取注解信息
- TableMapper tableMapper = buildTableMapper(object.getClass());
- // 從表注解里獲取表名等信息
- TableMapperAnnotation tma = (TableMapperAnnotation) tableMapper.getTableMapperAnnotation();
- String tableName = tma.tableName();
- StringBuffer tableSql = new StringBuffer();
- StringBuffer valueSql = new StringBuffer();
- tableSql.append("insert into ").append(tableName).append("(");
- valueSql.append("values(");
- boolean allFieldNull = true;
- // 根據字段注解和屬性值聯合生成sql語句
- for (String dbFieldName : tableMapper.getFieldMapperCache().keySet()) {
- FieldMapper fieldMapper = tableMapper.getFieldMapperCache().get(dbFieldName);
- String fieldName = fieldMapper.getFieldName();
- Object value = dtoFieldMap.get(fieldName);
- // 由於要根據字段對象值是否為空來判斷是否將字段加入到sql語句中,因此DTO對象的屬性不能是簡單類型,反而必須是封裝類型
- if (value == null) {
- continue;
- }
- allFieldNull = false;
- tableSql.append(dbFieldName).append(",");
- valueSql.append("#{").append(fieldName).append(",").append("jdbcType=")
- .append(fieldMapper.getJdbcType().toString()).append("},");
- }
- if (allFieldNull) {
- throw new RuntimeException("Are you joking? Object " + object.getClass().getName()
- + "'s all fields are null, how can i build sql for it?!");
- }
- tableSql.delete(tableSql.lastIndexOf(","), tableSql.lastIndexOf(",") + 1);
- valueSql.delete(valueSql.lastIndexOf(","), valueSql.lastIndexOf(",") + 1);
- return tableSql.append(") ").append(valueSql).append(")").toString();
- }
- public static String buildInsertSql(Object object) throws Exception {
- if (null == object) {
- throw new RuntimeException("Sorry,I refuse to build sql for a null object!");
- }
- Map dtoFieldMap = PropertyUtils.describe(object);
- // 從參數對象里提取注解信息
- TableMapper tableMapper = buildTableMapper(object.getClass());
- // 從表注解里獲取表名等信息
- TableMapperAnnotation tma = (TableMapperAnnotation) tableMapper.getTableMapperAnnotation();
- String tableName = tma.tableName();
- StringBuffer tableSql = new StringBuffer();
- StringBuffer valueSql = new StringBuffer();
- tableSql.append("insert into ").append(tableName).append("(");
- valueSql.append("values(");
- boolean allFieldNull = true;
- // 根據字段注解和屬性值聯合生成sql語句
- for (String dbFieldName : tableMapper.getFieldMapperCache().keySet()) {
- FieldMapper fieldMapper = tableMapper.getFieldMapperCache().get(dbFieldName);
- String fieldName = fieldMapper.getFieldName();
- Object value = dtoFieldMap.get(fieldName);
- // 由於要根據字段對象值是否為空來判斷是否將字段加入到sql語句中,因此DTO對象的屬性不能是簡單類型,反而必須是封裝類型
- if (value == null) {
- continue;
- }
- allFieldNull = false;
- tableSql.append(dbFieldName).append(",");
- valueSql.append("#{").append(fieldName).append(",").append("jdbcType=")
- .append(fieldMapper.getJdbcType().toString()).append("},");
- }
- if (allFieldNull) {
- throw new RuntimeException("Are you joking? Object " + object.getClass().getName()
- + "'s all fields are null, how can i build sql for it?!");
- }
- tableSql.delete(tableSql.lastIndexOf(","), tableSql.lastIndexOf(",") + 1);
- valueSql.delete(valueSql.lastIndexOf(","), valueSql.lastIndexOf(",") + 1);
- return tableSql.append(") ").append(valueSql).append(")").toString();
- }
plugin的實現
- public Object plugin(Object target) {
- // 當目標類是StatementHandler類型時,才包裝目標類,否者直接返回目標本身,減少目標被代理的
- // 次數
- if (target instanceof StatementHandler) {
- return Plugin.wrap(target, this);
- } else {
- return target;
- }
- }
- public Object plugin(Object target) {
- // 當目標類是StatementHandler類型時,才包裝目標類,否者直接返回目標本身,減少目標被代理的
- // 次數
- if (target instanceof StatementHandler) {
- return Plugin.wrap(target, this);
- } else {
- return target;
- }
- }