本文為學(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ú)讀/寫性能差不多