• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

fastquery: FastQuery 基于Java语言. 他的使命是:简化Java操作数据层.做为一个开发者, ...

原作者: [db:作者] 来自: 网络 收藏 邀请

开源软件名称:

fastquery

开源软件地址:

https://gitee.com/xixifeng.com/fastquery

开源软件介绍:

FastQuery logo

Apache Maven

<dependency>    <groupId>org.fastquery</groupId>    <artifactId>fastquery</artifactId>    <version>1.0.117</version> <!-- fastquery.version --></dependency>

Gradle/Grails

compile 'org.fastquery:fastquery:1.0.117'

FastQuery 数据持久层框架

FastQuery 基于Java语言.他的使命是:简化Java操作数据层.
提供少许Annotation,消费者只用关心注解的含义,这就使得框架的核心便于重构,便于持续良性发展.

FastQuery 主要特性如下:

  1. 遵循非侵入式原则,设计优雅或简单,极易上手
  2. 在项目初始化阶段采用ASM生成好字节码,因此支持编译前预处理,可最大限度减少运行期的错误,显著提升程序的强壮性
  3. 支持安全查询,防止SQL注入
  4. 支持与主流数据库连接池框架集成
  5. 支持 @Query 查询,使用 @Condition,可实现动态 where 条件查询
  6. 支持查询结果集以JSON类型返回
  7. 拥有非常优雅的Page(分页)设计
  8. 支持AOP,注入拦截器只需要标识几个简单的注解,如: @Before , @After
  9. 使用@Source可实现动态适配数据源.这个特性特别适合多租户系统中要求数据库彼此隔离其结构相同的场景里
  10. 支持@QueryByNamed命名式查询,SQL动态模板
  11. 支持存储过程
  12. 支持批量更新集合实体(根据主键,批量更新不同字段,不同内容).

运行环境要求

JRE 8+

配置文件

配置文件的存放位置

默认从classpath目录下去寻找配置文件. 配置文件的存放位置支持自定义, 如: System.setProperty("fastquery.config.dir","/data/fastquery/configs");, 它将会覆盖classpath目录里的同名配置文件. 如果项目是以jar包的形式启动,那么可以通过java命令的 -D 参数指定配置文件的目录, 如: java -jar Start.jar -Dfastquery.config.dir=/data/fastquery/configs.

c3p0-config.xml

完全支持c3p0官方配置,详情配置请参照c3p0官网的说明.

<?xml version="1.0" encoding="UTF-8"?><c3p0-config>      <named-config name="xk-c3p0">          <property name="driverClass">com.mysql.cj.jdbc.Driver</property>          <property name="jdbcUrl">jdbc:mysql://192.168.1.1:3306/xk?useSSL=false</property>          <property name="user">xk</property>          <property name="password">abc123</property>          <property name="acquireIncrement">50</property>          <property name="initialPoolSize">100</property>          <property name="minPoolSize">50</property>          <property name="maxPoolSize">1000</property>        <property name="maxStatements">0</property>          <property name="maxStatementsPerConnection">5</property>         </named-config>     <!-- 可以配置多个named-config节点,多个数据源 -->    <named-config name="name-x"> ... ... </named-config></c3p0-config>

druid.xml

用于配置支持Druid连接池,详细配置请参照 https://github.com/alibaba/druid

<beans>	 <bean name="xkdb1" id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"                                         init-method="init" destroy-method="close"> 	     <property name="url" value="jdbc:mysql://db.fastquery.org:3305/xk" />	     <property name="username" value="xk" />	     <property name="password" value="abc123" />	     <property name="filters" value="stat" />	     <property name="maxActive" value="20" />	     <property name="initialSize" value="1" />	     <property name="maxWait" value="60000" />	     <property name="minIdle" value="1" />	     <property name="timeBetweenEvictionRunsMillis" value="60000" />	     <property name="minEvictableIdleTimeMillis" value="300000" />	     <property name="testWhileIdle" value="true" />	     <property name="testOnBorrow" value="false" />	     <property name="testOnReturn" value="false" />	     <property name="poolPreparedStatements" value="true" />	     <property name="maxOpenPreparedStatements" value="20" />	 </bean>	 <!-- 再配置一个数据源 --> 	 <bean name="xkdb2" id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"                                         init-method="init" destroy-method="close"> 	     <property name="url" value="jdbc:mysql://db.fastquery.org:3305/xk" />	     <property name="username" value="xk" />	     <property name="password" value="abc123" />	 </bean></beans>

hikari.xml

用于配置支持HikariCP连接池,详细配置选项请参照 https://github.com/brettwooldridge/HikariCP
连接MySQL,为了得到更好的性能,推荐配置

<beans>	<bean name="xkdb2">		<property name="jdbcUrl" value="jdbc:mysql://192.168.1.1:3306/xk" />		<property name="dataSource.user" value="xk" />		<property name="dataSource.password" value="abc123" />		<property name="dataSource.cachePrepStmts" value="true" />		<property name="dataSource.prepStmtCacheSize" value="250" />		<property name="dataSource.prepStmtCacheSqlLimit" value="2048" />		<property name="dataSource.useServerPrepStmts" value="true" />		<property name="dataSource.useLocalSessionState" value="true" />		<property name="dataSource.rewriteBatchedStatements" value="false" />		<property name="dataSource.cacheResultSetMetadata" value="true" />		<property name="dataSource.cacheServerConfiguration" value="true" />		<property name="dataSource.elideSetAutoCommits" value="true" />		<property name="dataSource.maintainTimeStats" value="false" />	</bean>	<!-- 可以配置多个bean节点,提供多个数据源 -->	<bean name="name-x"> ... ... </bean></beans>

支持多连接池共存,如,同时让Druid,HikariCP工作,并配置多个数据源.

fastquery.json

配置数据源的作用范围

// @author xixifeng ([email protected])// 配置必须遵循标准的json语法.{  "scope":[		    // config 用于指定由谁来提供数据源,如,"c3p0","druid","hikari"等等		    {		        "config": "c3p0",            // 表示由c3p0负责提供数据源		        "dataSourceName": "xk-c3p0", // 数据源的名称		        "basePackages": [            // 该数据源的作用范围		            "org.fastquery.example",              // 包地址		            "org.fastquery.dao.UserInfoDBService" // 完整类名称 		            // 在这可以配置多个DB接口或包地址,以","号隔开		            // 提醒:在json结构中,数组的最后一个元素的后面不能加","		        ]		    },		    		     /*		      再配置一个数据源作用域		     */		     {		        "config" : "mySQLDriver",      // 表示由mySQLDriver负责提供数据源		        "dataSourceName": "shtest_db", // 数据源的名称		        "basePackages": [              // 该数据源的作用范围		            "org.fastquery.example.DataAcquireDbService"		             // 在这可以配置多个DB接口,以","号隔开		        ]		     },		    		     {		        "config": "c3p0",              // 表示由c3p0负责提供数据源		        "basePackages": [   		             "org.fastquery.dao2.UserInfoDB"		        ]		     }		  ] }

注意: 在fastquery.json中配置作用域,其中"dataSourceName"不是必须的,"dataSourceName"要么不指定,要指定的话那么必须正确.如果没有指定"dataSourceName",那么在调用接口的时候必须指定数据源的名称.下面的适配数据源章节会讲到."basePackages"若配置了包地址,那么对应的数据源会作用这个包的所有类,及所有子包中的类.

数据源的初始化是从"fastquery.json"开始的,根据从里面读到"dataSourceName"的值,取相应的配置,继而完成数据源的创建.如,创建一个名为"rex-db"的数据源:

{    "config": "c3p0",               "dataSourceName": "rex-db"}

在这里,"basePackages"不是必须的,该数据源可以当做是一个服务,供没有明确指定数据源的Repository使用.

入门例子

当看到一个例子时,切勿断章取义,多看一眼,往往会有意想不到的结果.

  • 准备一个实体
 public class Student {      private String no;      private String name;      private String sex;      private Integer age;      private String dept;      // getter / setter 省略...  } 

实体属性跟数据库映射的字段必须为包装类型,否则被忽略. 在实体属性上标识@Transient,表示该字段不参与映射.

  • DAO接口
 public interface StudentDBService extends org.fastquery.core.Repository {    @Query("select no, name, sex from student")    JSONArray findAll();    @Query("select no,name,sex,age,dept from student")    Student[] find();       }
  • 使用DAO接口.
public class StudentDBServiceTest {	// 获取实现类	private static StudentDBService studentDBService = FQuery.getRepository(StudentDBService.class);	@Test	public void test() {		// 调用 findAll 方法		JSONArray jsonArray = studentDBService.findAll();		// 调用 find 方法		Student[] students = studentDBService.find(); 	}}

注意:不用去实现StudentDBService接口.通过FQuery.getRepository获取DAO接口对应的实例,虽然每次获取实例消耗的性能微乎其微可以忽略不计,但是,作为一个接口并且频繁被调用,因此,建议把获取到的实例赋值给类成员变量,最好是用static修饰.FQuery.getRepository获得的实例是唯一的,不可变的.

一个接口不实现它的public abstract方法就毫无作用可言,因此,与之对应的实例对象是必须的,只不过是FastQuery内部替用户实现了.读者可能会问,这个自动生成的实例在什么时候生成? 动态生成的效率如何保持高效? 为此, 笔者做了相当多的功课:让所有DB实现类在项目初始化阶段进行,并且尽可能地对接口方法做静态分析,把有可能在运行期发生的错误尽最大努力提升到初始化阶段,生成代码前会检测SQL绑定是否合法有效、检测方法返回值是否符合常规、方法的参数是否满足模版的调用、是否正确地使用了分页...诸如此类问题.这些潜在问题一旦暴露,项目都启动不起来,错误信息将在开发阶段详细输出,并且必须干掉这些本该在生产环境才发生的错误,才能继续开发,迫使开发者必须朝正确的道路走,或者说框架的优良设计其核心理念引导开发者不得不写出稳健的程式.项目进入运行期,大量的校验就没必要写了,从而最大限度保证快速执行.

唯一的出路,只能引用接口,这就使得开发者编程起来不得不简单,因为面对的是一个高度抽象的模型,而不必去考虑细枝末节.接口可以看成是一个能解析SQL并能自动执行的模型,方法的参数、绑定的模版和标识的注解无不是为了实现一个目的:执行SQL,返回结果.

这种不得不面向接口的编程风格,有很多好处:耦合度趋向0,天然就是对修改封闭,对扩展开放,不管是应用层维护还是对框架增加新特性,这些都变得特别容易.隐藏实现,可以减少bug或者是能消灭bug,就如解决问题,不如消灭问题一般,解决问题的造诣远远落后于消灭问题,原因在于问题被解决后,不能证明另一个潜在问题在解决代码中不再出现,显然消灭问题更胜一筹.应用层只用写声明抽象方法和标识注解,试问bug从何而来?该框架最大的优良之处就是让开发者没办法去制造bug,至少说很难搞出问题来.不得不简便,没法造bug,显然是该项目所追求的核心目标之一.

不管用不用这个项目,笔者期望读者至少能快速地检阅一下该文档,有很多设计是众多同类框架所不具备的,希望读者从中得到正面启发或反面启发,哪怕一点点,都会使你收益.

针对本文@Query的由来

该项目开源后,有些习惯于繁杂编码的开发者表示,"使用@Query语义不强,为何不用@SQL,@Select,@Insert,@Update...?". SQL的全称是 Structured Query Language,本文的 @Query 就是来源于此. @Query只作为运行SQL的载体,要做什么事情由SQL自己决定.因此,不要片面的认为Query就是select操作. 针对数据库操作的注解没有必要根据SQL的四种语言(DDL,DML,DCL,TCL)来定义,定义太多,只会增加复杂度,并且毫无必要,如果是改操作加上@Modifying注解,反之,都是"查",这样不更简洁实用吗? 诸如此类:@Insert("insert into table (name) values('Sir.Xi')"),@Select("select * from table"),SQL的表达能力还不够吗? 就不觉得多出@insert@Select有拖泥带水之嫌? SQL的语义本身就很强,甚至连@Query@Modifying都略显多余,但是毕竟SQL需要有一个载体和一个大致的分类.

带条件查询

// sql中的?1 表示对应当前方法的第1个参数// sql中的?2 表示对应当前方法的第2个参数//       ?N 表示对应当前方法的第N个参数	// 查询返回数组格式@Query("select no,name,sex,age,dept from student s where s.sex=:sex and s.age > ?1")Student[] find(Integer age,@Param("sex")String sex); 	// 查询返回JSON格式@Query("select no, name, sex from student s where s.sex=:sex and s.age > ?2")JSONArray find(@Param("sex")String sex,Integer age);	// 查询返回List Map@Query("select no, name, sex from student s where s.sex=?1 and s.age > :age")List<Map<String, Object>> findBy(String sex,@Param("age")Integer age);// 查询返回List 实体@Query("select id,name,age from `userinfo` as u where u.id>?1")List<UserInfo> findSome(@Param("id")Integer id);

参数较多时不建议使用问号(?)引用参数,因为它跟方法的参数顺序有关,不便维护,可以使用冒号(:)表达式,跟顺序无关, ":name" 表示引用标记有@Param("name")的那个参数.
若返回List<Map<String, String>>Map<String, String>,会把查询出的字段值(value)包装成字符串.

注意: 在没有查询到数据的情况下,如果返回值是集合类型或JSON类型或者是数组类型,返回具体的值不会是null,而是一个空对象(empty object)集合或空对象JSON或者是长度为0的数组.
使用空对象来代替返回null,它与有意义的对象一样,并且能避免NullPointerException,阻止null肆意传播,可以减少运行期错误.反对者一般都从性能的角度来考虑,认为new一个空对象替代null,会增加系统的开销.可是,<<Effective Java>>的作者Josh Bloch说,在这个级别上担心性能问题是不明智的,除非有分析表明,返回空对象来替代返回null正是造成性能问题的源头.细心的人可能已经发现JDK新版本的API都在努力避免返回null.
举例说明:

// 针对该方法,如果没有查询到数据,返回值的结果是一个长度为0的Student[]@Query("sql statements")Student[] find(Integer age,String sex); // 针对该方法,如果没有查询到数据,返回值的结果是一个空Map(非null)@Query("sql statements")Map<String,Object> find(Integer id);// 针对该方法,如果没有查询到数据,返回值的结果是一个空List<Map>(非null)@Query("sql statements")List<Map<String, Object>> find(String sex);

注意: 查询单个字段,还支持返回如下类型:

  • List<String>,String[]String
  • List<Byte>,Byte[]Byte
  • List<Short>,Short[]Short
  • List<Integer>,Integer[]Integer
  • List<Long>,Long[]Long
  • List<Float>,Float[]Float
  • List<Double>,Double[]Double
  • List<Character>,Character[]Character
  • List<Boolean>,Boolean[]Boolean

除了改操作或count外,查单个字段不能返回基本类型,因为:基本类型不能接受null值,而SQL表字段可以为null.返回类型若是基本类型的包装类型,若返回null, 表示:没有查到或查到的值本身就是null.例如:

// 查询单个字段,若没有查到,就返回空List<String>(非null)@Query("select name from Student limit 3")List<String> findNames(); 

类属性名称与表字段不一致时,如何映射?

为了说明这个问题先准备一个实体

public class UserInformation {	private Integer uid;	private String myname;	private Integer myage;	// getters / setters	// ... ...}

而数据库中的表字段分别是id,name,age,通过SQL别名的方式,可以解决类属性名称与表字段不一致的映射问题.如下:

// 把查询到的结果映射给UserInformation@Query("select id as uid,name as myname,age as myage from UserInfo u where u.id = ?1")UserInformation findUserInfoById(Integer id);

动态条件查询

采用Annotation实现简单动态条件

看到这里,可别认为SQL只能写在Annotation(注解)里.FastQuery还提供了另二种方案: ① 采用@QueryByNamed(命名式查询),将SQL写入到模板文件中,并允许在模板文件里做复杂的逻辑判断,相当灵活. ② 通过QueryBuilder构建SQL.下面章节有详细描述.

@Query("select no, name, sex from Student #{#where} order by age desc")// 增加若干个条件@Condition("no like ?1")                            // ?1的值,如果是null, 该行条件将不参与运算@Condition("and name like ?2")                      // 参数 ?2,如果接收到的值为null,该条件不参与运算// 通过 ignoreNull=false 开启条件值即使是null也参与运算@Condition(value = "and age > ?3",ignoreNull=false) // ?3接收到的值若为null,该条件将保留@Condition("and name not like ?4") @Condition("or age between ?5 and ?6")Student[] findAllStudent(... args ...);

注意:

  • 如果参数是String类型,值若为null或""(空字符串),在默认情况下,都会使条件移除
  • ignoreNull=false : 参数值即便为null,条件也参与
  • ignoreEmpty=false : 参数值即使为"",条件也保留

@Condition(value="name = ?1",ignoreNull=false)表示?1接受到的值若是null,该条件也参与运算,最终会翻译成name is null.SQL中的null无法跟比较运算符(如=,<,或者<>)一起运算,但允许跟is null,is not null,<=>操作符一起运算,故,将name = null想表达的意思,解释成name is null.
@Condition(value="name != ?1",ignoreNull=false)?1的值为null,最终会解释成name is not null.

In查询作为一个条件单元时,请忽略null判断,如@Condition("or dept in(?4,?5,?6)"其中的一个参数为null就将条件移除显然不太合理.

结构包装

有时候我们需要返回如下结构的数据:

{	"departmentId":1,	"departmentName":"研发",	"emps":[		{			"name":"小明",			"id":1		},		{			"name":"张三",			"id":2		},		{			"name":"李思",			"id":3		}	]}

举例说明,部门对应员工是 1:N 关系,查询某一个部门下面的员工,可以这样写:

@Query("select d.id as departmentId, d.name as departmentName, emps[e.id, e.name] from `department` d left join employee e on d.id = e.departmentId where d.id = :departmentId")Department findDepartment(@Param("departmentId") Long departmentId);

其中 emps[e.id, e.name] 是关键,[] 确定集合 emps 里的元素,Department 类中需要有 emps 成员属性。

通过JAVA脚本控制条件增减

@Condition中的ignoreScript属性可以绑定一个JAVA脚本(不是JS),根据脚本运行后的布尔结果,来决定是否保留条件项.脚本运行后的结果如果是true,那么就删除该条件项,反之,保留条件项,默认脚本是false,表示保留该条件项. 注意: 脚本执行后得到的结果必须是布尔类型,否则,项目都启动不起来.
举例:

@Query("select id,name,age from `userinfo` #{#where}")@Condition("age > :age")@Condition(value="and name like :name",ignoreScript=":age > 18 && :name!=null && :name.contains(\"Rex\")")Page<UserInfo> find(@Param("age")int age,@Param("name")String name,Pageable pageable);

其中, :age引用的是@Param("age")int age的实参值.:name@Param("name")String name的实参值.这个脚本要表达的意思不言而喻. 不过脚本的解析能力还不能自动拆箱(unboxing),需要调用拆箱方法,在这里age变量如果是Integer类型,要想如上脚本能正确编译,必须这么做: ":age.intValue() > 18 && :name!=null && :name.contains(\"Rex\")", 请留意住:age.intValue(). 其他包装类型Short, Long, Byte, Boolean, Character, Float, Double 以此类推.

什么是JAVA脚本?

在这里将一段承载着程序的字符串称之为JAVA脚本.脚本在初始化阶段被解释成能在JVM里运行的字节码,在脚本里能通过:expression(冒号表达式)获取当前方法在运行时所接受到的所有参数,引用的参数可以是一个复杂对象,完全可以把:expression当成是对象的引用句柄.虽然允许把脚本写得很长,更支持写出较为复杂的逻辑,但是,不建议这么做,因为那样可读性极差,不便迭代维护.做再庞杂的程序,都应该拆分成若干小而简单的功能,然后以优良的设计将其串联起来.FastQuery自始自终会遵守简单,严谨,清晰的编程风格.

@Condition 中的 if...else

条件是否保留可以通过if条件来确定,if绑定的JAVA脚本运行后的结果若为true就保留该Condition,反之就取else的捆绑值,else如果没有值或者是空值,表示移除该Condition.
举例:

@Query("select id,name,age from `userinfo` #{#where}")@Condition(value="age > :age",if$=":age < 18", else$="name = :name")Page<UserInfo> findPage(@Param("age")int age,@Param("name")String name,Pageable pageable);

如果age的实参值小于18,则,保留该Condition,否则,该Condition的值变为name = :name.当然,else不是必须的,如果if运算为假,直接删除该行SQL条件.

自定义类控制条件增减

决定一个条件是否参与运算,有时候需要根据多个不同的参数进行某种计算来决定, 并且这种计算逻辑用JAVA脚本(非JS)难以表达或者不太乐意让JAVA脚本登场. 那么就使用@Condition中的ignore选项,指定一个类,它叫Judge,是一个裁判员,条件是否去除的决定权可以理所当然地委托给自定义的Judge类来处理.
举例: 若:年龄大于18及姓名不为空且包含"Rex".则,剔除条件and name like :name.
定制一个决定条件存活的类,需要遵循一些约定: 继承org.fastquery.where.Judge,当完成这一步,IDE就会提示开发者必须实现ignore方法, 否则,面对的是红叉. 这样的设计可以减少犯错的可能. 当ignore方法最终返回true时,则,删除相对应的条件;当最后返回false时,则,保留条件.

public class LikeNameJudge extends Judge {	@Override	public boolean ignore() {		// 获取方法中名称为"age"的参数值		int age = this.getParameter("age", int.class);		// 获取方法中名称为"name"的参数值		String name = this.getParameter("name", String.class);		return age > 18 && name!=null && name.contains("Rex");	}}

LikeNameJudgethis范围内可以获得当前DB方法的所有实参.这些参数都有资格决定条件的存亡.
指定 LikeNameJudge:

@Query("select id,name,age from `userinfo` #{#where}")@Condition("age > :age")@Condition(value="and name like :name",ignore=LikeNameJudge.class)Page<UserInfo> find(@Param("age")int age,@Param("name")String name,Pageable pageable);

其中,ignore选项默认指定DefaultJudge,它是一个无所事事的裁判员,当它是空气好了.

@Condition的值使用了${表达式},$表达式,不管方法的参数传递了什么都不会使条件移除,因为$表达式(或称之为EL表达式)仅作为简单模版使用,传null,默认会替换为""(空字符串).举例:

@Query("select * from `userinfo` #{#where}")@Condition("age between $age1 and ${age2}")List<Map<String, Object>> between(@Param("age1") Integer age1,@Param("age2") Integer age2);	

该例中@Condition使用到了$表达式,$age1,${age2}仅作为模板替换,age1为null,即便设置ignoreNull=true也不会影响条件的增减.总之,$ 表达式不会


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap