Spring Jpa 使用SQLFunction 添加自定义函数

放几个阿里云的优惠链接 代金券 / 高性能服务器2折起 / 高性能服务器5折

本例项目源码地址
本文内容前提建立在自己对Jpa和hibernate有所了解。由于自己比较喜欢使用Gradle作为构建工具,所以项目基本都使用Gradle为例。如果本文有存在错误,希望大家指出说明。

准备工作

使用Spring boot作为基本环境,添加相关依赖。数据库这里采用Mysql

dependencies {
	compile('org.springframework.boot:spring-boot-starter-data-jpa')
	compile 'mysql:mysql-connector-java'
}

了解 SQLFunction

从SQLFunction的注释中可以知道它用于进行HQL 和SQL函数的转换。
接口如下:

public interface SQLFunction {
	public boolean hasArguments();
	public boolean hasParenthesesIfNoArguments();
	public Type getReturnType(Type firstArgumentType, Mapping mapping) throws QueryException;
	/**
	 * Render the function call as SQL fragment.
	 * 

* Note, the 'firstArgumentType' parameter should match the one passed into {@link #getReturnType} * * @param firstArgumentType The type of the first argument * @param arguments The function arguments * @param factory The SessionFactory * * @return The rendered function call * * @throws org.hibernate.QueryException Indicates a problem rendering the * function call. */ public String render(Type firstArgumentType, List arguments, SessionFactoryImplementor factory) throws QueryException; }

getReturnType:表明了函数的返回值类型,例如MySQL中sqrt函数返回值为DOUBLE,一些基本的返回值都定义在StandardBasicTypes类中。
render:此方法将自定义的函数转换为具体数据库对应数据库函数。

创建自定义Dialect

在hibernate中,一些基本常用的sql 函数都会在Dialect定义注册,以mysql为例,一些常见的函数都在Dialect的构造方法中注册。

registerFunction( "abs", new StandardSQLFunction( "abs" ) );
registerFunction( "sign", new StandardSQLFunction( "sign", StandardBasicTypes.INTEGER ) );
registerFunction( "acos", new StandardSQLFunction( "acos", StandardBasicTypes.DOUBLE ) );
registerFunction( "asin", new StandardSQLFunction( "asin", StandardBasicTypes.DOUBLE ) );
registerFunction( "atan", new StandardSQLFunction( "atan", StandardBasicTypes.DOUBLE ) );
...

所以想要添加自定义函数,只需要继承相关Dialect,在构造中注册自己的函数即可。

public class CustomMysql5Dialect extends MySQL5Dialect {
	public CustomMysql5Dialect() {
		super();
		registerFunction("bitand", new SQLFunctionTemplate(IntegerType.INSTANCE, "(?1 & ?2)"));
		registerFunction("bitor", new SQLFunctionTemplate(IntegerType.INSTANCE, "(?1 | ?2)"));
		registerFunction("bitxor", new SQLFunctionTemplate(IntegerType.INSTANCE, "(?1 ^ ?2)"));
	}
}

上面注册了3个位运算函数,由于SQLFunction存在很多实现类,这里直接使用SQLFunctionTemplate做为实例。 (?1 & ?2) 表示传入2个参数,例如 id,3则会输出 id & 3

使用自定义Dialect

在spring boot中只需要在application.yml中指定database-platform即可

 jpa:
    database-platform: top.shenluw.demo.querydsl.CustomMysql5Dialect

具体的注入在HibernateJpaVendorAdapter.buildJpaPropertyMap方法内,具体解释了。

private Map<String, Object> buildJpaPropertyMap(boolean connectionReleaseOnClose) {
		Map<String, Object> jpaProperties = new HashMap<>();

		if (getDatabasePlatform() != null) {
			jpaProperties.put(AvailableSettings.DIALECT, getDatabasePlatform());
		}
		else {
			Class<?> databaseDialectClass = determineDatabaseDialectClass(getDatabase());
			if (databaseDialectClass != null) {
				jpaProperties.put(AvailableSettings.DIALECT, databaseDialectClass.getName());
			}
		}
		...
}

使用自定义函数

随便定义一个Repository,自定义一个方法,在Query注解内写入一个简单的查询语句,在条件上使用上面定义的bitand函数即可。

public interface AnimalRepo extends JpaRepository, QuerydslPredicateExecutor {
	@Query("select a from Animal as a where bitand(a.age,  ?2) = ?1")
	List findAllByAge(int age, int bit);
}

开启HQL日志可以看到上面的语句世界被转换成了如下语句,(animal0_.age & ?) 即自己写入的 bitand(a.age, ?2)

select animal0_.id as id1_0_, animal0_.age as age2_0_, animal0_.created_date as created_3_0_, animal0_.fly as fly4_0_, animal0_.name as name5_0_ from animal animal0_ where (animal0_.age & ?)=?

到次为止基本的使用方法都描述完了。但是这个方法对于支持多数据库就不是很好了,每当换一个数据库就继承一个Dialect,这样对开发很不友好。接下来讲一个比较投机取巧的方式来实现适配问题。

了解DialectResolver

在阅读了hibernate源码后发现,hibernate创建Dialect是通过DialectResolver接口实现的,所以只要自己实现这个接口然后把Dialect方式做出一些修改应该就可以完成想要的效果。

public interface DialectResolver extends Service {
	/**
	 * Determine the {@link Dialect} to use based on the given information.  Implementations are expected to return
	 * the {@link Dialect} instance to use, or {@code null} if the they did not locate a match.
	 *
	 * @param info Access to the information about the database/driver needed to perform the resolution
	 *
	 * @return The dialect to use, or null.
	 */
	Dialect resolveDialect(DialectResolutionInfo info);
}

该接口只有一个方法,一般框架里面的用到的实际类为StandardDialectResolver,就是根据Database枚举类中的定义创建对应的对象。

public class StandardDialectResolver implements DialectResolver {
	private static final CoreMessageLogger LOG = CoreLogging.messageLogger( StandardDialectResolver.class );

	/**
	 * Singleton access
	 */
	public static final StandardDialectResolver INSTANCE = new StandardDialectResolver();

	@Override
	public Dialect resolveDialect(DialectResolutionInfo info) {

		for ( Database database : Database.values() ) {
			Dialect dialect = database.resolveDialect( info );
			if ( dialect != null ) {
				return dialect;
			}
		}

		return null;
	}
}

所以我们自己实现 一个也非常简单。直接调用StandardDialectResolver中的创建方法,然后把自己的函数添加上去即可。由于registerFunction是一个protected,所以这里用了反射实现。

public class InjectSqlFunctionDialectResolver implements DialectResolver {

	public static final String SQL_FUNCTION_BITAND = "bitand";
	public static final String SQL_FUNCTION_BITOR  = "bitor";
	public static final String SQL_FUNCTION_BITXOR = "bitxor";

	@Override
	public Dialect resolveDialect(DialectResolutionInfo info) {
		Dialect dialect = StandardDialectResolver.INSTANCE.resolveDialect(info);
		Method  method  = ReflectionUtils.findMethod(dialect.getClass(), "registerFunction", String.class, SQLFunction.class);
		method.setAccessible(true);
		Map functions = dialect.getFunctions();
		if (!functions.containsKey(SQL_FUNCTION_BITAND)) {
			ReflectionUtils.invokeMethod(method, dialect, SQL_FUNCTION_BITAND, new SQLFunctionTemplate(IntegerType.INSTANCE, "(?1 & ?2)"));
		}
		if (!functions.containsKey(SQL_FUNCTION_BITOR)) {
			ReflectionUtils.invokeMethod(method, dialect, SQL_FUNCTION_BITOR, new SQLFunctionTemplate(IntegerType.INSTANCE, "(?1 | ?2)"));
		}
		if (!functions.containsKey(SQL_FUNCTION_BITXOR)) {
			ReflectionUtils.invokeMethod(method, dialect, SQL_FUNCTION_BITXOR, new SQLFunctionTemplate(IntegerType.INSTANCE, "(?1 ^ ?2)"));
		}
		return dialect;
	}
}

使用自定义DialectResolver

现在实现类已经创建完了,但是该怎么用上去还不知到。
阅读了hibernate相关文档后发现可以通过定义hibernate.dialect_resolvers属性来自定义DialectResolver。

尝试在JPA中配置hibernate.dialect_resolvers

spring:
  jpa:
    properties:
      hibernate:
        dialect_resolvers: top.shenluw.demo.querydsl.InjectSqlFunctionDialectResolver

运行后发现毫无效果,完全没有进到这里。开始怀疑属性配置有问题
一番debug调试后进到DialectResolverInitiator类看到自己注册的对象的确已经被加载进去

	@Override
	public DialectResolver initiateService(Map configurationValues, ServiceRegistryImplementor registry) {
		final DialectResolverSet resolver = new DialectResolverSet();

		applyCustomerResolvers( resolver, registry, configurationValues );
		resolver.addResolver(StandardDialectResolver.INSTANCE );

		return resolver;
	}

	private void applyCustomerResolvers(
			DialectResolverSet resolver,
			ServiceRegistryImplementor registry,
			Map configurationValues) {
		final String resolverImplNames = (String) configurationValues.get( AvailableSettings.DIALECT_RESOLVERS );

		if ( StringHelper.isNotEmpty( resolverImplNames ) ) {
			final ClassLoaderService classLoaderService = registry.getService( ClassLoaderService.class );
			for ( String resolverImplName : StringHelper.split( ", \n\r\f\t", resolverImplNames ) ) {
				try {
					resolver.addResolver(
							(DialectResolver) classLoaderService.classForName( resolverImplName ).newInstance()
					);
				}
				catch (HibernateException e) {
					throw e;
				}
				catch (Exception e) {
					throw new ServiceException( "Unable to instantiate named dialect resolver [" + resolverImplName + "]", e );
				}
			}
		}
	}

然而此时却完全没有进入到自己定义的逻辑中,只能再跟踪源码的调用过程来分析。

我们先分析原先是怎么创建Dialect的。

当程序先从application获取到database-platform后会进入到HibernateJpaVendorAdapter中使用。
如果定义了则直接把放入jpaProperties中如果没有则使用determineDatabaseDialectClass方法获取。

private Map<String, Object> buildJpaPropertyMap(boolean connectionReleaseOnClose) {
		Map<String, Object> jpaProperties = new HashMap<>();

		if (getDatabasePlatform() != null) {
			jpaProperties.put(AvailableSettings.DIALECT, getDatabasePlatform());
		}
		else {
			Class<?> databaseDialectClass = determineDatabaseDialectClass(getDatabase());
			if (databaseDialectClass != null) {
				jpaProperties.put(AvailableSettings.DIALECT, databaseDialectClass.getName());
			}
		}
		...
}

但这里最终只是把配置信息的保存在环境中,还没有使用。
通过源码搜索看到使用AvailableSettings.DIALECT地方只有一个,就在DialectFactoryImpl.buildDialect 方法中

	@Override
	public Dialect buildDialect(Map configValues, DialectResolutionInfoSource resolutionInfoSource) throws HibernateException {
		final Object dialectReference = configValues.get( AvailableSettings.DIALECT );
		if ( !isEmpty( dialectReference ) ) {
			return constructDialect( dialectReference );
		}
		else {
			return determineDialect( resolutionInfoSource );
		}
	}

如果dialectReference 不为空,就通过constructDialect创建,否则用determineDialect创建。

	private Dialect constructDialect(Object dialectReference) {
		final Dialect dialect;
		try {
			dialect = strategySelector.resolveStrategy( Dialect.class, dialectReference );
			if ( dialect == null ) {
				throw new HibernateException( "Unable to construct requested dialect [" + dialectReference + "]" );
			}
			return dialect;
		}
		catch (HibernateException e) {
			throw e;
		}
		catch (Exception e) {
			throw new HibernateException( "Unable to construct requested dialect [" + dialectReference + "]", e );
		}
	}

	private Dialect determineDialect(DialectResolutionInfoSource resolutionInfoSource) {
		if ( resolutionInfoSource == null ) {
			throw new HibernateException( "Access to DialectResolutionInfo cannot be null when 'hibernate.dialect' not set" );
		}

		final DialectResolutionInfo info = resolutionInfoSource.getDialectResolutionInfo();
		final Dialect dialect = dialectResolver.resolveDialect( info );

		if ( dialect == null ) {
			throw new HibernateException(
					"Unable to determine Dialect to use [name=" + info.getDatabaseName() +
							", majorVersion=" + info.getDatabaseMajorVersion() +
							"]; user must register resolver or explicitly set 'hibernate.dialect'"
			);
		}

		return dialect;
	}

上面可以看到当值为空时,才会用到DialectResolver,所以尝试把database-platform值设置为空时,可以发现程序进入到了自己的逻辑中,并成功执行。配置如下:

spring:
  jpa:
    database-platform:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect_resolvers: top.shenluw.demo.querydsl.InjectSqlFunctionDialectResolver

你可能感兴趣的:(spring)