Android數(shù)據(jù)庫SQLite

本文為學(xué)習(xí)小記,有錯(cuò)的請指正

關(guān)于sqlite

開源的,小型的,可嵌入的,關(guān)系型數(shù)據(jù)庫

效率高

程序驅(qū)動(dòng)

無數(shù)據(jù)類型:對字段類型不做檢查

事務(wù)操作

基本了解

  • 數(shù)據(jù)類型

Integer varchar float double char text ~

  • 增刪改查

//1:創(chuàng)建表
create table tableName(fieldName dataType 約束, fieldName dataType 約束, ...)
//例:
create table person(_id Integer primary key, name varchar(10), age Integer not null)

//2:刪除表
drop table tableName
//例:
drop table person

//3:插入數(shù)據(jù)
insert into tableName(field, field) values(value, value)
//例:
insert into person(_id, age) values(1, 20)
insert into person values(2, "name", 30)

//4:修改數(shù)據(jù)
update tableName set field = newValue where 條件
//例:
update person set name="ls" where _id = 1

//5:刪除數(shù)據(jù)
delete from tableName where 條件
//例:
delete from person where _id = 2

//6:查詢數(shù)據(jù)
select field from tableName where 條件 group by 分組字段 having 篩選條件 order by 排序字段
//例:
select * from person
select _id, name from person
select * from person where _id = 1
select * from person where _id<>1 //不等于1
select * from person where _id=1 and age>18
select * from person where name like "%name%"
select * from person where name is null
select * from person where age between 10 and 20
select * from person where age>18 order by _id
select * from person limit ?,? // 第一條index,每頁數(shù)據(jù)量

...

基礎(chǔ)開發(fā)

數(shù)據(jù)庫

系統(tǒng)提供SqliteOpenHelper用于打開或創(chuàng)建db,需要繼承該類實(shí)現(xiàn)自定義的helper

getReadableDataBase, getWritableDataBase 都可以用來打開或創(chuàng)建db,該db系統(tǒng)默認(rèn)指定存儲(chǔ)路徑:data/data/pakage/database

源碼邏輯:getReadableDatabase()和getWriteableDatabase()都是調(diào)用getDatabaseLocked(boolean writeable) 方法,傳不同的參數(shù)
getReadableDatabase() 會(huì)獲取用于操作SQLiteDatabase的實(shí)例。 
getReadableDatabase()會(huì)先以讀寫方式打開數(shù)據(jù)庫,若數(shù)據(jù)庫磁盤空間滿了,打開失敗,會(huì)繼續(xù)嘗試以只讀方式打開。若磁盤空間有了,會(huì)關(guān)閉只讀數(shù)據(jù)庫對象,返回可讀寫數(shù)據(jù)庫對象。
getWriteableDatabase()也是會(huì)以讀寫方式打開數(shù)據(jù)庫,如果磁盤滿了,會(huì)拋異常,不會(huì)返回?cái)?shù)據(jù)庫對象。
其實(shí)就是getReadableDatabase()會(huì)在拋異常的時(shí)候以只讀模式打開數(shù)據(jù)庫。而getWritableDatabase()不會(huì)

helper的onCreate回調(diào)db,可用于創(chuàng)建數(shù)據(jù)表等

也可以打卡指定路徑的db:

SQLiteDatabase.openDatabase("path", null, SQLiteDatabase.OPEN_READWRITE)

sql語句

熟能生巧,細(xì)心高效

api

db調(diào)用系統(tǒng)api,增刪改查等,內(nèi)部是用代碼拼接的sql語句,原理一樣

api參數(shù):ContentValues 是 HashMap類型,防sql注入,,但是要注意ContentValues擴(kuò)容問題

查詢相關(guān)

查詢結(jié)果是cursor,相當(dāng)于是查詢結(jié)果信息的表,需要特定解析轉(zhuǎn)化為bean對應(yīng)到業(yè)務(wù)中

SimpleCursorAdapter適配器

針對業(yè)務(wù)簡單的,信息字段不多的內(nèi)容,可以使用SimpleCursorAdapter適配器,,,跟SimpleAdapter同理,,,就是把cursor數(shù)據(jù)展示到ui上

注意:使用SimpleCursorAdapter,表中必須有個(gè)名稱叫"_id"的主鍵列

CursorAdapter

這個(gè)就靈活一點(diǎn),自定義adapter,可以拿到item_view,并bindView,,跟一般的自定義adapter差不多了

事務(wù)

db.execSQL()執(zhí)行sql語句底層也是基于事務(wù)的,所以,批量操作時(shí)候,頻繁的開關(guān)事務(wù),就顯得比較耗費(fèi)性能了,把批量操作放到一個(gè)事務(wù)中

...
//1:開啟事務(wù)
db.beginTransaction()
//2:批量操作
注意把處理放到開啟事務(wù)和提交事務(wù)之間就行
//3:提交當(dāng)前事務(wù)
db.setTransactionSuccessful()
//4:關(guān)閉事務(wù)
db.endTransaction()
...

有種檢測方法:在批量處理前后打印起止時(shí)間,比較一下加事務(wù)前后的時(shí)間,事務(wù)方式的批量處理,用時(shí)明顯少很多

數(shù)據(jù)庫升級(jí)

原理:

創(chuàng)建db時(shí)候,入?yún)ersion,系統(tǒng)會(huì)檢查數(shù)據(jù)庫文件中存儲(chǔ)的版本號(hào),如果比原版本號(hào)大,就會(huì)調(diào)用onUpgrade回調(diào),在這里進(jìn)行數(shù)據(jù)庫升級(jí)操作

場景:

version_1 》version_2

version_1 》version_3

數(shù)據(jù)庫升級(jí)就這兩種情況,一種是版本號(hào)臨近的,一種是版本號(hào)跨級(jí)的。升級(jí)的時(shí)候,建議方案是臨級(jí)別逐個(gè)升級(jí),如:version_1 》version_3,循環(huán)處理從version_1升級(jí)到version_2執(zhí)行的sql語句,然后處理version_2到version_3執(zhí)行的sql語句

@Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        try {
            // 歷次升級(jí)操作都需執(zhí)行,比如oldVersion為2,newVersion為5,則需將version為3、4、5時(shí)的改動(dòng)都執(zhí)行一遍
            for(int i = 1; i + oldVersion <= newVersion; i++) {
                updateOnVersion(db, i + oldVersion);
            }
        } catch (Exception e) {
            Logger.saveException(context, e, LogLevelEnum.ERROR_LEVEL);
        }
    }
    
private synchronized void updateOnVersion(SQLiteDatabase db, int version) {
        try {
            switch (version) {
                case 2:
                    sqlVersion2(db);
                    break;
                default:
                    break;
            }
        } catch (Exception e) {
            Logger.saveException(context, e);
        }
    }

具體升級(jí)操作:

數(shù)據(jù)庫升級(jí)一般也就下面幾種情況:

  • 增加表

就是普通的create table 語句

  • 刪除表
execSQL(db, "DROP TABLE IF EXISTS " + tempTableName, null);
  • 表增加字段
db.execSQL("alter table TableName add column field")

表增加字段是在表末尾加字段,沒辦法指定任意位置,當(dāng)然位置的意義也不大

注意:sql語句不能同時(shí)新增多個(gè)字段,只能單次新增一個(gè),所以,可以結(jié)合事務(wù)批量執(zhí)行上面的sql語句

  • 表修改字段

注意:sqlite是不支持修改字段的,本著只增不減的原則,要么增加字段,要么重建表然后遷移數(shù)據(jù)

  • 表刪除字段
alter table TableName drop column field 

新建表遷移數(shù)據(jù)

針對以上手段已經(jīng)無法滿足業(yè)務(wù)改動(dòng)需求了,只能新建表了,但是還想要原數(shù)據(jù),就需要遷移數(shù)據(jù):


-- 把原表改成另外一個(gè)名字作為暫存表 
ALTER TABLE old_table_name RENAME TO temp_table_name; 

-- 如果需要,可以刪除原表的索引 
DROP INDEX ix_name; 

-- 用原表的名字創(chuàng)建新表 
CREATE TABLE old_table_name ( field_name INTEGER PRIMARY KEY AUTOINCREMENT ,  other_field_name text notnull); 

-- 如果需要,可以創(chuàng)建新表的索引 
CREATE INDEX ix_name ON old_table_name(field_name); 

-- 將暫存表數(shù)據(jù)寫入到新表,很方便的是不需要去理會(huì)自動(dòng)增長的 ID 
INSERT INTO old_table_name SELECT * FROM  temp_table_name 

-- 刪除暫存表 
DROP TABLE temp_table_name;

//~~~~~~~~~~~~~~~~divide line~~~~~~~~~~~~~~~~~

protected void upgradeTables(SQLiteDatabase db, String tableName, String columns)  
{  
    try  
    {  
        db.beginTransaction();  

        // 1, 將原表先重命名為臨時(shí)表名  
        String tempTableName = tableName + "_temp";  
        String sql = "ALTER TABLE " + tableName +" RENAME TO " + tempTableName;  
        execSQL(db, sql, null);  

        // 2, 用原名創(chuàng)建新表,,(私有方法)
        onCreateTable(db); 

        // 3, 遷移數(shù)據(jù)  
        sql =   "INSERT INTO " + tableName +  
                " (" + columns + ") " +  
                " SELECT " + columns + " FROM " + tempTableName;  

        execSQL(db, sql, null);  

        // 4, 刪除臨時(shí)表  
        execSQL(db, "DROP TABLE IF EXISTS " + tempTableName, null);  

        db.setTransactionSuccessful();  
    }  
    catch (SQLException e)  
    {  
        e.printStackTrace();  
    }  
    catch (Exception e)  
    {  
        e.printStackTrace();  
    }  
    finally  
    {  
        db.endTransaction();  
    }  
}  

數(shù)據(jù)庫加密

需求:對于root的android手機(jī),app下的db是不安全的,網(wǎng)上資料大多就是兩種方案,第一種對要加密的表內(nèi)容加密,一點(diǎn)都不優(yōu)雅。。第二種,SQLCipher,全網(wǎng)好像也就這一種開源免費(fèi)的加密方案。。第三種:SSE(SQLite Encryption Extension),網(wǎng)上說微信也用的SQLCipher,不過不重要。SQLCipher夠香

SQLCipher

  • 加密性能高、開銷小,只要5-15%的開銷用于加密
  • 完全做到數(shù)據(jù)庫100%加密
  • 采用良好的加密方式(CBC加密模式: 學(xué)習(xí)文章
  • 使用方便,做到應(yīng)用級(jí)別加密
  • 采用OpenSSL加密庫提供的算法

使用

SQLCipher的api是基于SQLite的,所以使用跟直接使用SQLite很類似,,,注意幾點(diǎn):

  • 包名

就是原來使用SQLite的時(shí)候引入的包,都換為net.sqlcipher.~即可

android.database.Cursor 為 net.sqlcipher.Cursor
android.database.sqlite.SQLiteDatabase 為 net.sqlcipher.database.SQLiteDatabase
android.database.SQLiteOpenHelper 為 net.sqlcipher.database.SQLiteOpenHelper.csdn.net/qq_36699930/article/details/100744874
  • 在自定義SQLiteOpenHelper的構(gòu)造方法中,加載SQLCipher需要的so庫:
SQLiteDatabase.loadLibs(context)
  • 創(chuàng)建db時(shí),需要提供加密的秘鑰
//獲取寫數(shù)據(jù)庫
SQLiteDatabase db = dbHelper.getWritableDatabase("your_pwd");
//獲取可讀數(shù)據(jù)庫
SQLiteDatabase db = dbHelper.getReadableDatabase("your_pwd");

數(shù)據(jù)庫多線程方案

學(xué)習(xí)文章(原文鏈接:https://blog.csdn.net/u010205141/article/details/44182461)

關(guān)于sqlite多線程操作,主要有兩類問題,如下,,

SQLite是文件級(jí)別的鎖,數(shù)據(jù)庫通過數(shù)據(jù)庫級(jí)上的獨(dú)占性和共享鎖來實(shí)現(xiàn)獨(dú)立事務(wù)處理

多線程訪問數(shù)據(jù)庫,需要區(qū)分是數(shù)據(jù)庫是單連接還是多連接,意思就是SqliteOpenHelper是單例的還是每個(gè)線程初始化一個(gè)實(shí)例(也即是一個(gè)helper實(shí)例表示一個(gè)數(shù)據(jù)庫連接)

  • 單連接多線程并發(fā)寫

數(shù)據(jù)庫寫操作,是有獨(dú)占鎖的,所以多線程寫操作,也是挨個(gè)執(zhí)行的。。

  • 單連接多線程并發(fā)讀

同寫差不多

  • 多連接多線程并發(fā)讀

數(shù)據(jù)庫讀操作,也是有獨(dú)占鎖的,所以,要實(shí)現(xiàn)多線程并發(fā)讀,得使用多連接

  • 多連接多線程并發(fā)寫

使用多個(gè)SQLiteDatabase對象同時(shí)插入,會(huì)繞過數(shù)據(jù)庫同步鎖,報(bào)異常:database is locked

  • 多線程讀寫

似乎這才是想要的多線程數(shù)據(jù)庫操作,能一邊讀一邊寫,由上面幾種情況也可知道,實(shí)現(xiàn)多線程讀寫,寫操作需要是單連接,讀操作需要是多連接。但是依然異常:database is locked

android 11后,數(shù)據(jù)庫可以設(shè)置預(yù)寫操作,enableWriteAheadLogging (源碼注釋建議用打開數(shù)據(jù)庫的參數(shù)配置ENABLE_WRITE_AHEAD_LOGGING,效率更高),打開該設(shè)置,以實(shí)現(xiàn)單連接寫和多連接讀同時(shí)操作。下面這段話是源碼注釋解釋的:

當(dāng)啟用了寫前日志記錄(通過調(diào)用此方法)時(shí),寫操作發(fā)生在一個(gè)單獨(dú)的日志文件中,這允許并發(fā)地進(jìn)行讀操作。當(dāng)寫操作正在進(jìn)行時(shí),其他線程上的讀取器將感知到數(shù)據(jù)庫的狀態(tài)與寫操作開始前一樣。當(dāng)寫入完成時(shí),其他線程上的讀取器就會(huì)感知到數(shù)據(jù)庫的新狀態(tài)

可以看到,該方案依然不是同事讀寫,只是把讀寫分開,寫完,合并,才能讀到新寫的內(nèi)容

attempt to re-open an already-closed object

該問題跟上面說的單連接多線程訪問不是一回事兒:

SQLiteOpenHelper 單例模式,多線程打開的是同一個(gè)數(shù)據(jù)庫的連接,在多線程并發(fā)讀寫操作的時(shí)候,在Thread2還在使用數(shù)據(jù)庫連接時(shí),Thread1可能已經(jīng)把它給關(guān)閉。所以,需要控制db關(guān)閉的時(shí)機(jī),兩種方案:

方案一:記錄openDb的次數(shù),打開一次,+1,關(guān)閉一次,-1,關(guān)閉時(shí)候當(dāng)計(jì)數(shù)為0,再closeDb。。另外,針對多線程操作,openDb 》 多線程處理 》closeDb,,不要那么死板,非要處理一下關(guān)閉一下。

方案二:單線程的線程池 Executors.newSingleThreadExecutor()

針對上面場景的情況,采用CAS機(jī)制能解決,思想就是有人在使用db,就定期來看是否用完,用完了才輪到自己:

在并發(fā)環(huán)境下,某個(gè)線程對共享變量先進(jìn)行操作,如果沒有其他線程爭用共享數(shù)據(jù)那操作就成功;如果存在數(shù)據(jù)的爭用沖突,那就采取補(bǔ)償措施,比如不斷的重試機(jī)制,直到成功為止,因?yàn)檫@種樂觀的并發(fā)策略不需要把線程掛起,也就把這種同步操作稱為非阻塞同步(操作和沖突檢測具備原子性)。在硬件指令集的發(fā)展驅(qū)動(dòng)下,使得 "操作和沖突檢測" 這種看起來需要多次操作的行為只需要一條處理器指令便可以完成

數(shù)據(jù)庫性能

SQLiteStatement

使用方法

StringBuffer sql_insert = new StringBuffer();
sql_insert.append("INSERT INTO users(name,gender,age,phoneNumber,address) ");
sql_insert.append(" VALUES( ?, ?, ?, ?, ?)");

List<User> users = new ArrayList<User>();
for(int i = 0;i<1000;i++){
    User user = new User();
    user.setId(i);
    user.setName("name"+i);
    user.setGender(0);
    user.setAge(user.getRandomAge());
    user.setPhoneNumber("13800138000");
    user.setAddress("GuangDong ShenZhen No."+i);
    users.add(user);
}

for(User user:users){
    SQLiteStatement statement=db.compileStatement(sql_insert.toString());
    statement.bindString(1, user.getName());
    statement.bindLong(2, user.getGender());
    statement.bindLong(3, user.getAge());
    statement.bindString(4, user.getPhoneNumber());
    statement.bindString(5, user.getAddress());
    statement.executeInsert();
}

SQLiteStatement 學(xué)習(xí)文章(原文鏈接:https://blog.csdn.net/efeics/article/details/18995433)

注意:如果要復(fù)用statement對象,避免數(shù)據(jù)沒有及時(shí)清理而導(dǎo)致數(shù)據(jù)重復(fù),需要清理statement.clearBindings

SQLiteStatement statement=db.compileStatement(sql_insert.toString());
for(User user:users){
    statement.clearBindings
    statement.bindString(1, user.getName());
    ...
    statement.executeInsert();
}

事務(wù)

事務(wù)確保每一次操作都具有原子性

事務(wù)的實(shí)現(xiàn)是依賴于名為rollback journal文件,借助這個(gè)臨時(shí)文件來完成原子操作和回滾功能。既然屬于文件,就符合Unix的文件范型(Open-Read/Write- Close),因而對于批量的修改操作會(huì)出現(xiàn)反復(fù)打開文件讀寫再關(guān)閉的操作。然而好在,我們可以顯式使用事務(wù),將批量的數(shù)據(jù)庫更新帶來的journal文件打開關(guān)閉降低到1次

事務(wù)相比預(yù)編譯SQLiteStatement性能提升更明顯

事務(wù)+預(yù)編譯SQLiteStatement,在sqlite普通使用中,是性能特別高的方案

索引

索引的作用相當(dāng)于圖書的目錄,索引會(huì)創(chuàng)建新的數(shù)據(jù)結(jié)構(gòu)來保存索引字段的值和指向?qū)?yīng)數(shù)據(jù)記錄的指針,所以,查詢的時(shí)候直接根據(jù)索引數(shù)據(jù)結(jié)構(gòu)的指針快速查到數(shù)據(jù)記錄,所以效率很高,另外,索引是有序的,所以,二分查找索引,索引查找數(shù)據(jù)記錄。效率更高

使用索引,查詢的性能提升非常顯著,但是刪改性能提升沒那么高,因?yàn)閿?shù)據(jù)刪改,索引也要相應(yīng)的改變,對于插入,索引可能反而會(huì)降低性能,并且索引讓db的大小增加了2倍多

針對客戶端中的數(shù)據(jù)庫,往往數(shù)據(jù)量不多,增加索引,反而降低性能。。綜合來看,針對數(shù)據(jù)量大,對查詢速度要求高的,可以考慮索引

及時(shí)關(guān)閉Cursor

關(guān)閉Cursor釋放資源

不關(guān)閉Cursor,并不會(huì)造成內(nèi)存泄露,系統(tǒng)在SQLiteCursor的finalize方法里(GC會(huì)調(diào)用該方法)做了close

注意:當(dāng)自定義SQLiteCursor的時(shí)候,如果沒覆寫finalize,那cursor釋放不了,一直這樣搞,IO資源總會(huì)耗盡

異步

對于大數(shù)據(jù)量的數(shù)據(jù)操作,也要考慮異步操作,避免導(dǎo)致ANR??傊莡i操作的都要考慮異步

WAL

Write Ahead Logging。上面為了解決多線程讀寫異常使用的技術(shù),該機(jī)制下:

寫:wal >>> delete
讀:wal > delete
讀寫:wal模式充分競爭,與單獨(dú)讀/寫性能差不多

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容