[JAVA][学习·练手·挖坑] 作个数据库帮助库雏形

在以往的编码中,使用过 spring-data-jpa,也用过 hibernatemybatis。在简单的数据库操做中,spring-data-jpa 是用起来最爽的,毕竟在 IntelliJ IDEA 中能够得到以下体验:java

clipboard.png

瞧瞧,实体类属性推导,查询条件推导。声明完接口就能够用了,一行sql都不用敲,多爽 : Pmysql

在这里就不讨论这三个框架的优劣了,毕竟就我目前的使用场景而言,也体会不太出来到底谁好用...毕竟复杂的
SQL查询都是要 相似hql或者XML 的解决方案来作的。spring

本着挖坑学习的精神,今天开始会试着一步一步作出一个本身的数据库帮助库 (不敢叫框架,毕竟#行业标准里太多 feature,实力不够,作不来 ORZ).sql

今天就作个雏形吧,雏形的意思就是:看起来好像完成了一些功能,但只是实验性得编码 : P数据库

说明

这个帮助库就命名为 ice 吧,请原谅 起名字困难症 ORZsegmentfault

这是一个 笔记 类型的文章,全部可能会有一些 啊 写到这里才想起来 这样的状况...缓存

本文只引用 mysql-connecterlombok 这两个包。
前者是数据库驱动,因为这是个挖坑性质的东西,因此只针对 MYSQL 作功能了;
后者是代码生成框架,挺好用的,强烈推荐mybatis

也就是说, ice 并不使用常见的数据库链接池,好比 druidcp30。而是本身实现一个缓存链接获取器,毕竟挖坑就挖深点嘛哈哈。
本文假定读者具有必定的 Java 能力,好比 反射代理 这两个点,有兴趣能够看看我以前的文章app

配置 Configuration

用过前边所说的三个框架的同窗确定配过配置文件对吧,我通常配合 spring-boot 使用 spring-data-jpa,因此在 application.properties 配置;其余两个框架则是在传统的 SSHSSM 环境下配置 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
相关文章
相关标签/搜索