在以往的编码中,使用过 spring-data-jpa
,也用过 hibernate
和 mybatis
。在简单的数据库操作中,spring-data-jpa
是用起来最爽的,毕竟在 IntelliJ IDEA
中可以获得如下体验:
瞧瞧,实体类属性推导,查询条件推导。声明完接口就可以用了,一行sql
都不用敲,多爽 : P
在这里就不讨论这三个框架的优劣了,毕竟就我目前的使用场景而言,也体会不太出来到底谁好用...毕竟复杂的SQL
查询都是要 类似hql
或者XML
的解决方案来做的。
本着挖坑学习的精神,今天开始会试着一步一步做出一个自己的数据库帮助库 (不敢叫框架,毕竟#行业标准里太多 feature,实力不够,做不来 ORZ).
今天就做个雏形吧,雏形的意思就是:看起来好像完成了一些功能,但只是实验性得编码 : P
说明
这个帮助库就命名为 ice
吧,请原谅 起名字困难症 ORZ
这是一个 笔记 类型的文章,所有可能会有一些 啊 写到这里才想起来 这样的情况...
本文只引用
mysql-connecter
和lombok
这两个包。
前者是数据库驱动,由于这是个挖坑性质的东西,所以只针对MYSQL
做功能了;
后者是代码生成框架,挺好用的,强烈推荐也就是说,
ice
并不使用常见的数据库连接池,比如druid
、cp30
。而是自己实现一个缓存连接获取器
,毕竟挖坑就挖深点嘛哈哈。
本文假定读者具备一定的 Java 能力,比如 反射、代理 这两个点,有兴趣可以看看我之前的文章。
配置 Configuration
用过前边所说的三个框架的同学肯定配过配置文件对吧,我一般配合 spring-boot
使用 spring-data-jpa
,所以在 application.properties
配置;其他两个框架则是在传统的 SSH
、SSM
环境下配置 application-*.xml
。
既然是雏形,那么 ice
前期就直接 code-based configuration
了 (才不是偷懒...)
/**
* Created by krun on 2017/9/22.
*/
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Configuration {
private String driverClass; //驱动类名
private String connectionURL; //连接url
private String username; //数据库用户名
private String password; //数据库密码
}
好,配置就搞定啦,毕竟常见的连接参数都可以直接在 connectionURL
中附带嘛。
连接供应者 ConnectionProvider
/**
* Created by krun on 2017/9/22.
*/
public class ConnectionProvider{
/**
* 不直接用构造器而是用这种方式获取实例,纯粹是我个人喜好,感觉这样更有 "通过配置得到" 的意思。
*/
public static CachedConnection configure (Configuration configuration) {
return new CachedConnection(configuration);
}
private Class driverClass = null;
private Configuration configuration;
private volatile Connection connection;
private CachedConnection (Configuration configuration) {
this.configuration = configuration;
try {
// 加载驱动
this.driverClass = Class.forName(this.configuration.getDriverClass( ));
} catch (ClassNotFoundException e) {
throw new RuntimeException("无法加载 JDBC 驱动: " + this.configuration.getDriverClass( ));
}
}
// 内部用来获取一个新连接
private synchronized Connection create ( ) {
// 检查是否已经加载驱动,没有的话抛出异常。
if (driverClass == null) {
throw new RuntimeException("尚未加载 JDBC 驱动.");
} else {
try {
// 获取一个新连接
return DriverManager.getConnection(this.configuration.getConnectionURL( ),
this.configuration.getUsername( ), this.configuration.getPassword( ));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
// 暴露给外界获取一个连接,在这里进行 "是否有可用连接" 和 "连接有效性检查"
public synchronized Connection provide( ) throws SQLException {
if (connection == null) {
connection = createConnection( );
} else if (connection.isClosed( )) {
connection = createConnection( );
}
return connection;
}
}
Repository模板 Repository
这个完全是受 spring-data-jpa
的影响,我觉得"方法映射数据库操作"的映射方式是最吼的,只是 JPA
的接口更简洁些。
/**
* Created by krun on 2017/9/22.
*/
public interface Repository<E, I extends Serializable> {
List<E> findAll(); //获取表内所有元素
E save(E e); //保存元素,当元素存在id时,尝试更新(update);不存在id时,尝试插入(insert)
long delete(E e); //删除元素
boolean exist(E e); //判断给定元素是否存在
}
考虑到实现难度,现在不打算做"方法名解析到sql语句"。因此还是直接引入一个 @Query
注解来设置方法对应的 SQL
操作:
/**
* Created by krun on 2017/9/22.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Query {
// 暂时也不做别名处理了
String value();
}
约定 @Query
注解中的 SQL 语句使用 %s
占位符指明表名(这由 Repository<E, I> 中 E 解析而来),用 ?
占位符指定参数,这是为了方便直接把语句放入PreparedStatement
使用。
那么结合一下,现在的模板应该是这样的:
/**
* Created by krun on 2017/9/22.
*/
public interface Repository<E, I extends Serializable> {
@Query("SELECT * FROM %s")
List<E> findAll();
...
}
Repository工厂 RepositoryFactory
现在用户可以继承 Repository
接口来声明一个指定实体类的 repository
,我们需要一个工厂类来为这些接口类创建代理对象(Proxy
)以注入我们的方法拦截器。
/**
* Created by krun on 2017/9/22.
*/
public class RepositoryFactory {
//全局工厂的名字
private static final String GLOBAL_FACTORY = "GLOBAL";
//用来保存给定名称和其对应的工厂实例
private static final LinkedHashMap<String, RepositoryFactory> factoryMap;
static {
factoryMap = new LinkedHashMap<>();
}
// 这与之前 Connection.configure 的写法一样,纯粹个人喜好。
public static RepositoryFactory configure(Configuration configure) {
return RepositoryFactory.configure(GLOBAL_FACTORY, configure);
}
public static RepositoryFactory configure(String name, Configuration configure) {
if (RepositoryFactory.factoryMap.get(name) == null) {
synchronized ( RepositoryFactory.factoryMap ) {
if (RepositoryFactory.factoryMap.get(name) == null) {
RepositoryFactory.factoryMap.put(name, new RepositoryFactory(ConnectionProvider.configure(configure)));
} else {
throw new RuntimeException(name + " 的工厂已经被初始化了,不能再对其进行配置。");
}
}
}
return RepositoryFactory.factoryMap.get(name);
}
public synchronized static RepositoryFactory get() {
return RepositoryFactory.get(GLOBAL_FACTORY);
}
public synchronized static RepositoryFactory get(String name) {
return RepositoryFactory.factoryMap.get(name);
}
// 每个工厂类实例都持有一个自己的 连接提供者,因为多数情况下全局只会有一个工厂类实例...
@Getter
private ConnectionProvider connectionProvider;
//用于保存每个工厂实例所创建的 repository 实例,用以复用,避免重复创建 repository 实例。
private final LinkedHashMap<Class<? extends Repository>, Repository> repositoryMap;
private RepositoryFactory(ConnectionProvider connectionProvider) {
this.connectionProvider = connectionProvider;
this.repositoryMap = new LinkedHashMap<>();
}
// 为 Repository 接口创建代理实例,并注入我们自己的方法拦截器:RepositoryInvocationHandler
@SuppressWarnings("unchecked")
private <E, I extends Serializable, T extends Repository<E, I>> T getProxy(Class<T> repositoryClass) {
return (T) Proxy.newProxyInstance(repositoryClass.getClassLoader(),
new Class[] {repositoryClass},
new RepositoryInvocationHandler(this, repositoryClass));
}
// 获取给定 repository 类型的代理实例
@SuppressWarnings("unchecked")
public <E, I extends Serializable, T extends Repository<E, I>> T getRepository(Class<T> repositoryClass) {
T repository;
if ((repository = (T) repositoryMap.get(repositoryClass)) == null) {
synchronized ( repositoryMap ) {
if ((repository = (T) repositoryMap.get(repositoryClass)) == null) {
repository = getProxy(repositoryClass);
repositoryMap.put(repositoryClass, repository);
}
}
}
return repository;
}
}
Repository的灵魂 RepositoryInvocationHandler
我们刚才在 RepositoryFactory.getProxy
中创建了一个RepositoryInvocationHandler
实例,并传入了RepositoryFactory
实例以及代理的Repository
类型。
这因为在方法拦截器中,我们需要获取一些东西:
- 操作的实体类的类型,因为它的全小写形式就是实体类所代表的表的名字
- 通过工厂类实例获取一个
connection
/**
* Created by krun on 2017/9/22.
*/
public class RepositoryInvocationHandler implements InvocationHandler {
private RepositoryFactory factory;
//用于保存repository的泛型信息,后面可以比较方便地获取,虽然也可以通过 "method.getDeclaringClass()" 来获取,但总觉得麻烦了些。
private Class<? extends Repository> invokeRepositoryClass;
public RepositoryInvocationHandler (RepositoryFactory factory, Class<? extends Repository> invokeRepositoryClass) {
this.factory = factory;
this.invokeRepositoryClass = invokeRepositoryClass;
}
public Object invoke (Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName( );
// 根据方法名选择合适的 handle方法,以后应该是要改成表驱动,不然太多 if-else 了 ORZ
// 说起来,表驱动的话,就有合适的地方暴露接口给用户修改方法映射逻辑了。
if (methodName.startsWith("find")) {
return handleFind(method, args);
} else if (methodName.startsWith("save")) {
} else if (methodName.startsWith("delete")) {
} else if (methodName.startsWith("exist")) {
}
return null;
}
// 通过保存的 invokeRepositoryClass 获取其持有的泛型信息
private String getEntityName () {
if (! Repository.class.isAssignableFrom(this.invokeRepositoryClass)) {
throw new RuntimeException(String.format("接口 [%s] 并没有继承 Repository", this.invokeRepositoryClass.getName( )));
}
// 这里没有做太多考虑,暂时没遇到问题而已...
ParameterizedType parameterizedType = (ParameterizedType) this.invokeRepositoryClass.getGenericInterfaces()[0];
return ((Class)parameterizedType.getActualTypeArguments()[0]).getSimpleName().toLowerCase();
}
@SuppressWarnings("unchecked")
private Object handleFind (Method method, Object... args) {
// 获取方法上的 @Query 注解
Query query = method.getAnnotation(Query.class);
if (query == null) {
throw new IllegalArgumentException("也许你忘了为 " + method.getDeclaringClass( ).getSimpleName( ) + "." + method.getName( ) + "() 设置 @Query 注解");
}
// java 7的 "try-with-resource" 语法糖,挺方便的,不用操心 connection 关没关了
// 突然想起来,这样写的话好像... ConnectionProvider 就没用了啊 ... ORZ
try (Connection connection = factory.getConnectionProvider().provide()) {
PreparedStatement preparedStatement = (PreparedStatement) connection
//简单得替换一下表名占位符
.prepareStatement(String.format(query.value(), getEntityName()));
// 粗暴得把参数都塞进去...
// 以后估计要做个 switch-case 把参数类型检查做一下
for (int i = 1; i <= args.length; i++) {
preparedStatement.setObject(i, args[i - 1]);
}
System.out.println(preparedStatement.asSql());
// 把结果打出来看看
ResultSet resultSet = preparedStatement.executeQuery();
ResultSetMetaData metaData = resultSet.getMetaData();
while (resultSet.next()) {
for (int i = 1; i <= metaData.getColumnCount(); i++) {
System.out.print(String.valueOf(resultSet.getObject(i)) + "\t");
}
System.out.println();
}
resultSet.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
// 同样的简单粗暴,只为了看效果哈哈
try {
// 注:这种写法在 "List<Student> findAll()" 这种情况会报错,因为 List 是接口,无法为其创建实例
return method.getReturnType().newInstance();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace( );
}
return null;
}
}
最后
/**
* Created by krun on 2017/9/22.
*/
public class App {
@Data
public static class Student {
private String id;
private String name;
}
interface StudentRepository extends Repository<Student, String> {
@Query("SELECT * FROM %s WHERE gender = ?")
List<Student> findByGender(String gender);
@Query("SELECT * FROM %s WHERE id > ?")
List<Student> findByIdAfter(String id);
@Query("SELECT * FROM %s WHERE name = ?")
Student findByName(String name);
}
public static void main(String[] args ) {
RepositoryFactory factory = RepositoryFactory.configure(Configuration.builder()
.driverClass("com.mysql.jdbc.Driver")
.connectionURL("jdbc:mysql://localhost:3306/hsc")
.username("gdpi")
.password("gdpi")
.build());
StudentRepository studentRepository = factory.getRepository(StudentRepository .class);
studentRepository .findByName("krun");
}
}
> SELECT * FROM student WHERE name = 'krun'
> 20152200000 计算机技术系 男 2015 软件技术 krun
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。