放几个阿里云的优惠链接 代金券 / 高性能服务器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的注释中可以知道它用于进行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:此方法将自定义的函数转换为具体数据库对应数据库函数。
在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
在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,这样对开发很不友好。接下来讲一个比较投机取巧的方式来实现适配问题。
在阅读了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;
}
}
现在实现类已经创建完了,但是该怎么用上去还不知到。
阅读了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