在以往的編碼中,使用過 spring-data-jpa
,也用過 hibernate
和 mybatis
。在簡單的數據庫操做中,spring-data-jpa
是用起來最爽的,畢竟在 IntelliJ IDEA
中能夠得到以下體驗:java
瞧瞧,實體類屬性推導,查詢條件推導。聲明完接口就能夠用了,一行sql
都不用敲,多爽 : Pmysql
在這裏就不討論這三個框架的優劣了,畢竟就我目前的使用場景而言,也體會不太出來到底誰好用...畢竟複雜的SQL
查詢都是要 相似hql
或者XML
的解決方案來作的。spring
本着挖坑學習的精神,今天開始會試着一步一步作出一個本身的數據庫幫助庫 (不敢叫框架,畢竟#行業標準裏太多 feature,實力不夠,作不來 ORZ).sql
今天就作個雛形吧,雛形的意思就是:看起來好像完成了一些功能,但只是實驗性得編碼 : P數據庫
這個幫助庫就命名爲 ice
吧,請原諒 起名字困難症 ORZsegmentfault
這是一個 筆記 類型的文章,全部可能會有一些 啊 寫到這裏纔想起來 這樣的狀況...緩存
本文只引用
mysql-connecter
和lombok
這兩個包。
前者是數據庫驅動,因爲這是個挖坑性質的東西,因此只針對MYSQL
作功能了;
後者是代碼生成框架,挺好用的,強烈推薦mybatis也就是說,
ice
並不使用常見的數據庫鏈接池,好比druid
、cp30
。而是本身實現一個緩存鏈接獲取器
,畢竟挖坑就挖深點嘛哈哈。
本文假定讀者具有必定的 Java 能力,好比 反射、代理 這兩個點,有興趣能夠看看我以前的文章。app
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
這個徹底是受 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(); ... }
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; } }
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