Android 原生 SQLite 資料庫的一次封裝實踐
本文首發於 vivo網際網路技術 微信公眾號
連結: https://mp.weixin.qq.com/s/CL4MsQEsrWS8n7lhXCOQ_g
作者:Li Bingyan
本文主要講述原生SQLite資料庫的一次ORM封裝實踐,給使用原生資料庫操作的業務場景(如:本身是一個SDK)帶來一些啟示和參考意義,以及跟隨框架的實現思路對資料庫操作、APT、泛型等概念更深一層的理解。
實現思路:透過動態代理獲取請求介面引數進行SQL拼湊,並以介面返回值(泛型)型別的RawType和ActualType來適配呼叫方式和執行結果,以此將實際SQL操作封裝在其內部來簡化資料庫操作的目的。
一、背景
毫無疑問,關於Android資料庫現在已經有很多流行好用的ORM框架了,比如:Room、GreenDao、DBFlow等都提供了簡潔、易用的API,尤其是谷歌開源的Room是目前最主流的框架。
既然已經有了這麼多資料庫框架了,為什麼還要動手封裝所謂自己的資料庫框架呢?對於普通 APP 的開發確實完全不需要,這些框架中總有一款可以完全滿足你日常需求;但如果你是一個SDK開發者,而且業務是一個比較依賴資料庫操作的場景,如果限制不能依賴第三方SDK(主要考量維護性、問題排查、穩定性、體積大小),那就不得不自己去寫原生SQLite操作了,這將是一個既繁瑣又容易出錯的過程(資料庫升級/降級/開啟/關閉、多執行緒情況、拼湊SQL語句、ContentValues插資料、遊標遍歷/關閉、Entity轉換等)。
為了在SDK的開發場景中避免上述繁瑣且容易出錯的問題,於是就有了接下來的一系列思考和改造。
二、預期目的
- 能簡化原生的增刪改查冗長操作,不要再去寫容易出錯的中間邏輯步驟
- 自動生成資料庫的建表、升級/降級邏輯
- 易用的呼叫介面(支援同步/非同步、執行緒切換)
- 穩定可靠,無效能問題
三、方案調研
觀察我們日常業務程式碼可以發現:一次資料庫查詢與一次網路請求在流程上是極為相似的,都是經過構造請求、發起請求、中間步驟、獲取結果、處理結果等幾個步驟。因此感覺可以將資料庫操作以網路請求的方式進行抽象和封裝,其詳細對比如下表所示:
透過上述相似性的對比並綜合現有ORM框架來考慮切入口,首先想到的是使用註解:
主流Room使用的是 編譯時註解(更有利於效能),但在具體編碼實現Processor過程中發現增刪改查操作的出參和入參處理有點過於繁瑣(參考 ),不太適用於本身就是一個SDK的場景,最終pass掉了。
執行時註解處理相對更簡單一些(介面和引數較容易適配、處理流程也可以直接寫我們熟悉的安卓原生程式碼),而且前面已經有了大名鼎鼎的網路請求庫Retrofit使用執行時註解實現網路請求的典型範例,因此可以依葫蘆畫瓢嘗試實現一下資料庫增刪改查操作,也是本次改造最終的實現方案。
相信大部分安卓客戶端開發同學都用過Retrofit(網路請求常用庫),其大概原理是:使用動態代理獲取介面對應的Method物件為入口,並透過該Method物件的各種引數(註解修飾)構造出Request物件拋給okhttp做實際請求,返回值則透過Conveter和Adapter適配請求結果(bean物件)和呼叫方式,如:Call<List<Bean>>、Observable<List<Bean>>等。
它以這種方式將網路請求的內部細節封裝起來,極大簡化了網路請求過程。根據其相似性,資料庫操作(增刪改查)也可以使用這個機制來進一步封裝。
對於資料庫的建表、升級、降級等這些容易出錯的步驟,最好是不要讓使用者自己去手動寫這部分邏輯,方案使用編譯時註解來實現(Entitiy類和欄位屬性、版本號透過註解對應起來),在編譯期間自動生成SQLiteOpenHelper的實現類。
綜合以上兩部分基本實現了所有痛點操作不再需要呼叫者去關注(只需關注傳參和返回結果),於是將其獨立成一個資料庫模組,取名Sponsor( [ˈspɑːnsər] ),寓意一種分發器或排程器方案,目前已在團隊內部使用。
四、Sponsor呼叫示例
1、Entity定義:
//Queryable:表示一個可查詢的物件,有方法bool convert(Cursor cursor),將cursor轉換為Entitiy //Insertable:表示一個可插入的物件,有方法ContentValues convert(),將Entitiy轉換為ContentValues public class FooEntity implements Queryable, Insertable { /** * 資料庫自增id */ private int id; /** * entitiy id */ private String fooId; /** * entity內容 */ private String data; //其他屬性 //getter()/setter() }
2、介面定義,宣告增刪改查介面:
/** * 插入 * @return 最後一個row Id */ @Insert(tableName = FooEntity.TABLE) Call<Integer> insertEntities(List<FooEntity> entities); /** * 查詢 * @return 獲取的entitiy列表 */ @Query("SELECT * FROM " + FooEntity.TABLE + " WHERE " + FooEntity.CREATE_TIME + " > " + Parameter1.NAME + " AND " + FooEntity.CREATE_TIME + " < " + Parameter2.NAME + " ORDER BY " + FooEntity.CREATE_TIME + " ASC LIMIT " + Parameter3.NAME) Call<List<FooEntity>> queryEntitiesByRange(@Parameter1 long start, @Parameter2 long end, @Parameter3 int limit); /** * 刪除 * @return 刪除記錄的條數 */ @Delete(tableName = FooEntity.TABLE, whereClause = FooEntity.ID + " >= " + Parameter1.NAME + " AND " + FooEntity.ID + " <= " + Parameter2.NAME) Call<Integer> deleteByIdRange(@Parameter1 int startId, @Parameter2 int endId);
3、建立FooService例項:
Sponsor sponsor = new Sponsor.Builder(this) .allowMainThreadQueries() //是否執行在主執行緒操作,預設不允許 //.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) //rxjava //.addCallAdapterFactory(Java8CallAdapterFactory.create()) //java8 //.addCallAdapterFactory(LiveDataCallAdapterFactory.create()) //livedata .logger(new SponsorLogger()) //日誌輸出 .build(); //呼叫create()方法建立FooService例項,實際上是返回了FooService的動態代理物件 FooService mFooService = sponsor.create(FooService.class);
4、插入Entitiy資料:
//構造Entity列表 List<FooEntity> entities = new ArrayList<>(); //add entities //同步方式 //rowId為最終的自增id(同原生insert操作返回值) //final int rowId = mFooService.insertEntities(entities).execute(); //非同步方式 mFooService.insertEntities(entities).enqueue(new Callback<Integer>() { @Override public void onResponse(Call<Integer> call, Integer rowId) { //success } @Override public void onFailure(Call<Integer> call, Throwable t) { //failed } });
5、查詢引數指定資料庫記錄,並轉換為Entitiy物件列表:
List<FooEntity> entities; //entities為查詢結果集合 entities = mFooService.queryEntitiesByRange(1, 200, 100).execute();
6、刪除引數指定資料庫記錄,返回總共刪除的記錄條數:
//cout為刪除的條數 int count = mFooService.deleteByIdRange(0, 100).execute();
注:
- 以上所有操作都支援根據具體的場景進行同步/非同步呼叫。
- 增、刪、改操作的Call<?>返回值引數(泛型引數)還可以直接指定為Throwable,如果內部異常可以透過它返回,成功則為空
五、核心實現點
基本原理仍是借鑑了Retrofit框架的實現,透過動態代理拿到Method物件的各種引數進行SQL拼湊,並透過Converter和Adapter適配執行結果,整體框架有如下幾module構成:
- sponsor:主體實現
- sponsor_annotaiton:註解定義,包括執行時註解和編譯時註解
- sponsor_compiler:資料庫建表、升級/降級等邏輯的Processor實現
- sponsor_java8、sponsor_livedata、sponsor_rxjava2:適配幾種主流的呼叫方式
1、動態代理入口
public <T> T create(final Class<T> daoClass, final Class<? extends DatabaseHelper> helperClass) { final Object obj = Proxy.newProxyInstance(daoClass.getClassLoader(), new Class<?>[]{daoClass}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getDeclaringClass() == Object.class) { return method.invoke(this, args); } DaoMethod<Object, Object> daoMethod = (DaoMethod<Object, Object>) loadDaoMethod(method); final DatabaseHelper helper = loadDatabaseHelper(daoClass, helperClass); Call<Object> call = new RealCall<>(helper, mDispatcher, mAllowMainThreadQueries, mLogger, daoMethod, args); return daoMethod.adapt(call); } }); return (T) obj; }
2、介面適配
由於動態代理會返回介面的Method物件和引數列表args[],可以透過這兩個引數拿到上述標識的所有元素,具體方法如下所示:
獲取方法的註解: method.getAnnotations() 獲取形參列表:已傳過來 獲取引數註解和型別:method.getParameterAnnotations() method.getGenericParameterTypes() 獲取呼叫方式:method.getGenericReturnType()後,再呼叫Type.getRawType() //Call 獲取結果型別:method.getGenericReturnType()後,再呼叫Type.getActualTypeArguments() //List<FooEntitiy>
3、返回結果適配
private Converter<Response, ?> createQueryConverter(Type responseType, Class<?> rawType) { Converter<Response, ?> converter = null; if (Queryable.class.isAssignableFrom(rawType)) { //返回單個實體物件 //其他處理邏輯 converter = new QueryableConverter((Class<? extends Queryable>) responseType); } else if (rawType == List.class) { //返回一個實體列表 //其他處理邏輯 converter = new ListQueryableConverter((Class<? extends Queryable>) argumentsTypes[0]); } else if (rawType == Integer.class) { //相容 SELECT COUNT(*) FROM table的形式 converter = new IntegerConverter(); } else if (rawType == Long.class) { converter = new LongConverter(); } return converter; }
ListQueryableConverter實現,主要是遍歷Cursor構建返回結果列表:
static final class ListQueryableConverter implements Converter<Response, List<? extends Queryable>> { @Override public List<? extends Queryable> convert(Response value) throws IOException { List<Queryable> entities = null; Cursor cursor = value.getCursor(); if (cursor != null && cursor.moveToFirst()) { entities = new ArrayList<>(cursor.getCount()); try { do { try { //反射建立entitiy物件 Queryable queryable = convertClass.newInstance(); final boolean convert = queryable.convert(cursor); if (convert) { entities.add(queryable); } } catch (Exception e) { e.printStackTrace(); } } while (cursor.moveToNext()); } catch (Exception e) { e.printStackTrace(); } } /* * 避免返回null */ if (entities == null) { entities = Collections.emptyList(); } return entities; } }
4、執行增刪改查操作
final class RealCall<T> implements Call<T> { @Override public T execute() { /** * 實際的增刪改查操作 */ Response response = perform(); T value = null; try { value = mDaoMethod.toResponse(response); } catch (Exception e) { e.printStackTrace(); } finally { //遊標關閉 if (response != null) { Cursor cursor = response.getCursor(); if (cursor != null) { try { cursor.close(); } catch (Exception e) { e.printStackTrace(); } } } //資料庫關閉 if (mDatabaseHelper != null) { try { mDatabaseHelper.close(); } catch (Exception e) { e.printStackTrace(); } } } return value; } /** * 具體資料庫操作方法 * @return */ private Response perform() { switch (mDaoMethod.getAction()) { case Actions.QUERY: { //.. Cursor cursor = query(String sql); } case Actions.DELETE: { //... int count = delete(simple, sql, tableName, whereClause); } case Actions.INSERT: { //... } case Actions.UPDATE: { //... } } return null; } /** * 具體的查詢操作 */ private Cursor query(String sql) { //... SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); final Cursor cursor = db.rawQuery(sql, null); //... return cursor; } /** * 具體的刪除操作 */ private int delete(boolean simple, String sql, String tableName, String whereClause) { SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); int result = 0; try { db.beginTransaction(); //... result = db.delete(tableName, whereClause, null); db.setTransactionSuccessful(); } finally { try { db.endTransaction(); } catch (Throwable t) { t.printStackTrace(); } } return result; } }
六、效能測試對比
- 測試手機:vivo X23
- 安卓版本:Android 9
- 處理器:驍龍670,2.0GHz,8核
- 測試方法:每個對比項測試5組資料,每組5輪測試,然後取平均值(四捨五入)
說明:
- 表中第4條測試(查出全部10w條資料)差異較大(相差79ms),其原因是原生介面的Entity物件是直接new出來的,而sponsor內部只能透過Entity的newInstance()介面去反射建立,導致了效能差距,但平均算下來,每newInstance()建立1000個物件才多了1ms,影響還是很小的。(嘗試使用Clone的方式最佳化,但效果仍不明顯)
- sponsor方式效能均略低於原生方式,原因是其需要動態拼湊SQL語句的效能消耗,但消耗極少。
七、在專案(SDK)中的應用實踐
該專案內部使用的資料庫是一個多庫多表的架構,資料庫操作(增刪改查、建表、升級/降級等)均是呼叫SQLiteOpenHelper原生介面寫的程式碼邏輯,導致相關操作需要寫很多的模板程式碼才能拿到最終結果,邏輯比較冗長;因此,在重構版本我們使用sponsor替換掉了這些原生呼叫,以此簡化這些繁瑣易出錯操作。目前執行良好,暫沒有發現明顯嚴重問題。
八、擴充套件知識——泛型的型別擦除
關於型別擦除,感覺很多人都有一些誤區,特別是客戶端開發平時涉及較少,感覺都不太理解:
根據我們的常識都知道Java的泛型在執行時是型別擦除的,編譯後就不會有具體的型別資訊了(都是Object或者某個上界型別)。
那麼問題來了,既然型別都擦除了,那retrofit又是怎樣能在執行時拿到方法泛型引數型別(包括引數型別和返回型別)的呢?比如內部可以根據函式的返回型別將json轉為對應bean物件。
起先也很難理解,於是透過查詢資料、技術群交流、寫demo驗證後才基本弄明白,總結為一句話:型別擦除其實只是把泛型的形參擦除了(方便和1.5以下版本相容),原始的位元組碼中還是會保留類結構(類、方法、欄位)的泛型型別資訊,具體儲存在Signature區域,可以使用Type的子類介面在執行時獲取到泛型的型別資訊。
1、retrofit請求介面一般定義如下:
可以看到這個函式的返回型別和引數型別都帶有泛型引數。
2、反編譯這個apk,並用JD-GUI工具開啟可以找到對應方法如下:
很多人看到這裡會覺得 泛型的型別資訊確實已經被完全清除了 。不過這個工具只是展示了簡單的類結構資訊(僅包含類、函式、欄位)而已,我們可以更進一步看一下該類對應的位元組碼來確認下,直接使用AS開啟apk,展開classes.dex找到對應類,右鍵->"Show ByteCode"檢視:
可以看到在Signature區域儲存了這個方法的所有引數資訊,其中就有泛型的型別資訊。
任何類、介面、構造器方法或欄位的宣告如果包含了泛型型別,則會生成Signature屬性,為它記錄泛型簽名資訊,不過函式內的區域性變數泛型資訊將不會被記錄下來。
3、下面看一下Type介面的繼承關係,以及提供的介面功能:
Class:最常見的型別,一個Class類的物件表示虛擬機器中的一個類或介面。
ParameterizedType:表示是引數化型別,如:List<String>、Map<Integer,String>這種帶有泛型的型別,常用方法有:
-
Type getRawType()——返回引數化型別中的原始型別,例如List<String>的原始型別為List。
-
Type[] getActualTypeArguments()——獲取引數化型別的型別變數或是實際型別列表,如Map<Integer, String>的實際泛型列表是Integer和String。
TypeVariable:表示的是型別變數,如List<T>中的T就是型別變數。
GenericArrayType:表示是陣列型別且組成元素是ParameterizedType或TypeVariable,例如List<T>或T[],常用方法有:
- Type getGenericComponentType()一個方法,它返回陣列的組成元素型別。
WildcardType:表示萬用字元型別,例如? extends Number 和 ? super Integer。常用方法有:
- Type[] getUpperBounds()——返回型別變數的上邊界。
- Type[] getLowerBounds()——返回型別變數的下邊界。
九、參考資料
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912579/viewspace-2709765/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Android 封裝AsyncTask操作Sqlite資料庫Android封裝SQLite資料庫
- C# SQLite資料庫 訪問封裝類C#SQLite資料庫封裝
- Android實用的SQLite資料庫工具類AndroidSQLite資料庫
- android sqlite資料庫 新增資料AndroidSQLite資料庫
- sqlite封裝SQLite封裝
- Android 中使用 SQLite 資料庫AndroidSQLite資料庫
- Android資料庫ContentProvider封裝原理Android資料庫IDE封裝
- 【Android】資料儲存(三) 資料庫(SQLite)Android資料庫SQLite
- 封裝框架的實踐封裝框架
- 封裝 uniapp 請求庫的最佳實踐封裝APP
- Sqlite封裝1-基本封裝-SqliteToolSQLite封裝
- sqlite 資料庫的資料字典SQLite資料庫
- Python資料庫程式設計全指南SQLite和MySQL實踐Python資料庫程式設計SQLiteMySql
- sqlite操作--- oracle資料庫中的資料導進sqliteSQLiteOracle資料庫
- 如何封裝資料庫操作封裝資料庫
- Android開源資料庫 GreenDao實踐Android資料庫
- 非同步的 SQL 資料庫封裝非同步SQL資料庫封裝
- android原生的資料庫實現(ContentProvider+SQLiteOpenHelper)Android資料庫IDESQLite
- 一次Android動畫工具類的封裝Android動畫封裝
- Android資料庫高手祕籍(1):SQLite命令Android資料庫SQLite
- Android資料庫高手祕籍(一):SQLite命令Android資料庫SQLite
- 在Android中檢視和管理sqlite資料庫AndroidSQLite資料庫
- Android資料庫Sqlite的基本用法及升級策略Android資料庫SQLite
- Flutter Dio封裝實踐Flutter封裝
- Oracle資料庫靜默安裝實踐Oracle資料庫
- Android 中的升級資料庫最佳方法實踐Android資料庫
- 記一次資料、邏輯、檢視分離的原生JS專案實踐JS
- 資料庫實踐資料庫
- 華為雲GaussDB NoSQL雲原生多模資料庫的超融合實踐SQL資料庫
- 在 Android Studio 上除錯資料庫 ( SQLite )Android除錯資料庫SQLite
- android SQLite資料庫應用於草稿箱AndroidSQLite資料庫
- Android原生SQLite常用SQL語句AndroidSQLite
- 【Java】操作Sqlite資料庫JavaSQLite資料庫
- SQLite資料庫管理器:SQLPro for SQLite for MacSQLite資料庫Mac
- 平安雲原生資料庫開發與實踐資料庫
- openGauss資料庫在CentOS上的安裝實踐資料庫CentOS
- .NET雲原生應用實踐(三):連線到PostgreSQL資料庫SQL資料庫
- 關於資料庫操作的封裝程式碼資料庫封裝