[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
相關文章
相關標籤/搜索