天天看點

android room 簡書,android Room庫使用問題

資料

學習過程記錄

照着各種文章寫,簡單的實作都一樣,可跑起來總是挂

java.lang.RuntimeException: cannot find implementation for com.charliesong.roomtest.room.JavaDatabase. JavaDatabase_Impl does not exist

at androidx.room.Room.getGeneratedImplementation(Room.java:94)

at androidx.room.RoomDatabase$Builder.build(RoomDatabase.java:723)

就是我們那個database類裡擷取執行個體Room.databaseBuilder().build(),調用build方法的時候就挂了。

提示很明顯,這個database按道理系統應該自動生成一個Impl類的,可我們沒有自動生成,可也不知道咋讓他自動生成

public abstract class JavaDatabase extends RoomDatabase

各種解決都不行啊,不過好像和kotlin有關,最後我把room相關的類,都改成java寫,然後就可以了。

如下

android room 簡書,android Room庫使用問題

image.png

後記

後來發現,room庫分kotlin版本和java版本的,引用的不同的東西,具體用啥,下邊有room的文檔位址可以查

kotlin寫的

apply plugin: 'kotlin-kapt'

//room

def room_version = "2.2.5"

implementation "androidx.room:room-runtime:$room_version"

kapt "androidx.room:room-compiler:$room_version"

// optional - Kotlin Extensions and Coroutines support for Room

implementation "androidx.room:room-ktx:$room_version"

java寫的

def room_version = "2.2.5"

implementation "androidx.room:room-runtime:$room_version"

annotationProcessor "androidx.room:room-compiler:$room_version"

整理下基本操作

最新的room版本可以到官網查詢,20190827目前都2.2了

room

第一步app的build.gradle下添加庫

def room_version = "1.1.1"

// or, for latest rc, use "1.1.1-rc1"

implementation "android.arch.persistence.room:runtime:$room_version"

// annotationProcessor "android.arch.persistence.room:compiler:$room_version"

kapt "android.arch.persistence.room:compiler:$room_version"

// optional - RxJava support for Room

implementation "android.arch.persistence.room:rxjava2:$room_version"

// optional - Guava support for Room, including Optional and ListenableFuture

implementation "android.arch.persistence.room:guava:$room_version"

// Test helpers

testImplementation "android.arch.persistence.room:testing:$room_version"

或者是androidx的

def room_version = "2.1.0-alpha02"

implementation "androidx.room:room-runtime:$room_version"

kapt "androidx.room:room-compiler:$room_version" // Kotlin 的話用kapt

// 如果需要用到 rxjava

implementation "androidx.room:room-rxjava2:$room_version"

// 如果需要用到 guava

implementation "androidx.room:room-guava:$room_version"

// 需要用到相關測試工具的話

testImplementation "androidx.room:room-testing:$room_version"

如果用kotlin的話,最上邊應該是這樣的

apply plugin: 'com.android.application'

apply plugin: 'kotlin-kapt'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {

compileSdkVersion 27

defaultConfig {

applicationId "com.charliesong.demo0327"

minSdkVersion 18

targetSdkVersion 27

versionCode 2

versionName "1.1"

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

multiDexEnabled true

//指定room.schemaLocation生成的檔案路徑

javaCompileOptions {

annotationProcessorOptions {

arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]

}

}

}

添加實體類

當一個類用@Entity注解并且被@Database注解中的entities屬性所引用,Room就會在資料庫中為那個entity建立一張表。

預設Room會為entity中定義的每一個field都建立一個column。如果一個entity中有你不想持久化的field,那麼你可以使用@Ignore來注釋它們

另外記得entity必須有一個@PrimaryKey 主鍵字段

import android.arch.persistence.room.ColumnInfo;

import android.arch.persistence.room.Entity;

import android.arch.persistence.room.PrimaryKey;

@Entity

public class Userjava {

@PrimaryKey(autoGenerate = true)

public int uid;

public String firstName;

public String lastName;

public int age;

@ColumnInfo(name = "region")//列的名字可以改

public String address;

}

添加資料通路對象dao

import android.arch.lifecycle.LiveData;

import android.arch.persistence.room.Dao;

import android.arch.persistence.room.Delete;

import android.arch.persistence.room.Insert;

import android.arch.persistence.room.Query;

import android.arch.persistence.room.Update;

import java.util.List;

@Dao

public interface UserJavaDao {

@Insert

void insertAll(Userjava... userjava);

@Query("select * from Userjava")

LiveData> getUsersFromSync();

@Delete

int delete(Userjava userjava);//參數可以是數組,集合,傳回的是删除成功的條數

@Update

int update(Userjava... userjava);//參數可以是數組,集合,傳回的是update成功的條數

}

database類

版本号必須>=1

import android.arch.persistence.db.SupportSQLiteDatabase;

import android.arch.persistence.room.Database;

import android.arch.persistence.room.Room;

import android.arch.persistence.room.RoomDatabase;

import android.arch.persistence.room.migration.Migration;

import android.support.annotation.NonNull;

import com.charliesong.demo0327.app.MyApplication;

//所有的entity注解的類,都得在這裡聲明

@Database(entities = {Userjava.class},version =4)

public abstract class JavaDatabase extends RoomDatabase {

//定義了幾個Dao注解的類,這裡就寫幾個抽象方法

public abstract UserJavaDao userJavaDao();

private static JavaDatabase javaDatabase;

public static JavaDatabase instance(){

if(javaDatabase==null){

synchronized (JavaDatabase.class){

if(javaDatabase==null){

javaDatabase= Room.databaseBuilder(MyApplication.myApplication,JavaDatabase.class,"test").

addCallback(new Callback() {

@Override

public void onCreate(@NonNull SupportSQLiteDatabase db) {

super.onCreate(db);

System.out.println("onCreate==========="+db.getVersion()+"==="+db.getPath());

}

@Override

public void onOpen(@NonNull SupportSQLiteDatabase db) {

super.onOpen(db);

System.out.println("onOpen==========="+db.getVersion()+"==="+db.getPath());

}

})

.allowMainThreadQueries()//允許在主線程查詢資料

.addMigrations(migration)//遷移資料庫使用,下面會單獨拿出來講

.fallbackToDestructiveMigration()//遷移資料庫如果發生錯誤,将會重新建立資料庫,而不是發生崩潰

.build();

}

}

}

return javaDatabase;

}

//資料庫更新用的

static Migration migration=new Migration(1,4) {

@Override

public void migrate(@NonNull SupportSQLiteDatabase database) {

System.out.println("migrate============"+database.getVersion());

database.execSQL("ALTER TABLE Userjava "+ " ADD COLUMN address TEXT");

}

};

}

然後就可以測試拉。

使用livedata比較友善,隻處理資料庫的增删改,發生變化livedata自動會變化,我們弄個observer就ok了,清單會自動重新整理的

UtilRoomDB.getUserDao().usersFromSync.observe(this, Observer {

myAdapter.submitList(it)

})

我用的listAdapter

inner class MyAdapter(callback: DiffUtil.ItemCallback): ListAdapter(callback)

順道記錄些其他問題

Error:(22, 17) 警告: Schema export directory is not provided to the annotation processor so we cannot export the schema.

You can either provide `room.schemaLocation` annotation processor argument OR set exportSchema to false.

方法1

添加 exportSchema = false 就是不需要這個表的json資料

@Database(entities = { YourEntity.class }, version = 1, exportSchema = false)

public abstract class MovieDatabase extends RoomDatabase {

...

}

android {

compileSdkVersion 26

buildToolsVersion "26.0.2"

defaultConfig {

applicationId "com.xingen.architecturecomponents"

minSdkVersion 15

targetSdkVersion 26

versionCode 1

versionName "1.0"

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

//指定room.schemaLocation生成的檔案路徑

javaCompileOptions {

annotationProcessorOptions {

arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]

}

}

}

}

運作以後就能看到下邊的東西了,json檔案裡可以看到我們定義的表以及其包含的字段,主鍵等資訊

android room 簡書,android Room庫使用問題

image.png

版本更新

列印下可以看到,第一次操作資料庫的時候,先create,再open

System.out.println("onCreate==========="+db.getVersion()+"==="+db.getPath());

onCreate===========0===/data/user/0/com.charliesong.demo0327/databases/test

onOpen===========1===/data/user/0/com.charliesong.demo0327/databases/test

更新操作

如果你改了版本,然後沒有寫Migration,那麼更新以後資料庫就被清空了。

這兩個方法,要麼别寫,要寫了你就得有對應的migration,

// .addMigrations(migration,migration2)//遷移資料庫使用,下面會單獨拿出來講

// .fallbackToDestructiveMigration()//遷移資料庫如果發生錯誤,将會重新建立資料庫,而不是發生崩潰

我們一般更新資料庫,應該是增加了字段或者修改了字段之類的。

//資料庫更新用的,從版本1更新到版本2

static Migration migration=new Migration(1,2) {

@Override

public void migrate(@NonNull SupportSQLiteDatabase database) {

System.out.println("migrate12============"+database.getVersion());

database.execSQL("ALTER TABLE Userjava "+ " ADD COLUMN country TEXT");

}

};

版本2更新到版本4,打算删除表中的一列

如果想改某列的類型,名稱啥的也一樣操作

.addMigrations(migration,migration2,migration3)

好像drop column不好使,是以就建立個臨時表,複制下資料然後删除老的,再把新表名字改回去

static Migration migration2=new Migration(2,3) {

@Override

public void migrate(@NonNull SupportSQLiteDatabase database) {

System.out.println("migrate23============"+database.getVersion());

database.execSQL("create table aaaa(uid int primary key,firstName text,lastName text,age text,address text)");

database.execSQL("insert into aaaa select uid ,firstName ,lastName ,age,address from Userjava");

database.execSQL("drop table Userjava");

database.execSQL("alter table aaaa rename to Userjava");

}

};

static Migration migration3=new Migration(3,4) {

@Override

public void migrate(@NonNull SupportSQLiteDatabase database) {

System.out.println("migrate34============"+database.getVersion());

// database.execSQL("ALTER TABLE Userjava "+ " ADD COLUMN address TEXT");

}

};

日志如下,2個print都出來了,然後挂了

看了下,我們上邊建立那個aaaa的表,資料類型有點問題,那就改一下

Process: com.charliesong.demo0327, PID: 30844

java.lang.IllegalStateException: Migration didn't properly handle Userjava(com.charliesong.demo0327.room.Userjava).

Expected:

TableInfo{name='Userjava', columns={address=Column{name='address', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0}, lastName=Column{name='lastName', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0}, firstName=Column{name='firstName', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0}, uid=Column{name='uid', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1}, age=Column{name='age', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0}}, foreignKeys=[], indices=[]}

Found:

TableInfo{name='Userjava', columns={address=Column{name='address', type='text', affinity='2', notNull=false, primaryKeyPosition=0}, lastName=Column{name='lastName', type='text', affinity='2', notNull=false, primaryKeyPosition=0}, firstName=Column{name='firstName', type='text', affinity='2', notNull=false, primaryKeyPosition=0}, uid=Column{name='uid', type='int', affinity='3', notNull=false, primaryKeyPosition=1}, age=Column{name='age', type='text', affinity='2', notNull=false, primaryKeyPosition=0}}, foreignKeys=[], indices=[]}

at com.charliesong.demo0327.room.JavaDatabase_Impl$1.validateMigration(JavaDatabase_Impl.java:75)

at android.arch.persistence.room.RoomOpenHelper.onUpgrade(RoomOpenHelper.java:87)

at android.arch.persistence.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.onUpgrade(FrameworkSQLiteOpenHelper.java:133)

at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:256)

at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:163)

at android.arch.persistence.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableSupportDatabase(FrameworkSQLiteOpenHelper.java:96)

at android.arch.persistence.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.java:54)

at android.arch.persistence.room.RoomDatabase.query(RoomDatabase.java:233)

at com.charliesong.demo0327.room.UserJavaDao_Impl$4.compute(UserJavaDao_Impl.java:165)

at com.charliesong.demo0327.room.UserJavaDao_Impl$4.compute(UserJavaDao_Impl.java:151)

at android.arch.lifecycle.ComputableLiveData$2.run(ComputableLiveData.java:100)

at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)

at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)

at java.lang.Thread.run(Thread.java:818)

這是成功的日志

11-15 09:15:27.546 com.charliesong.demo0327 I/System.out: migrate23============2

11-15 09:15:27.556 com.charliesong.demo0327 I/System.out: migrate34============2

11-15 09:15:27.606 com.charliesong.demo0327 I/System.out: onOpen===========4===/data/user/0/com.charliesong.demo0327/databases/test

new Migration(4,6) 裡邊2個版本号到底指的啥

為了測試,

@1 版本從4直接改到8,然後添加2個Migration

new Migration(4,6)和new Migration(4,8)

列印了下,發現隻走了4到8

@2 然後版本從8改成11,添加下邊2個

new Migration(8,10) new Migration(9,11)

然後2個都沒走,然後資料被清空了

看下Migration的查詢條件

存儲的時候是存在這個集合裡的

private SparseArrayCompat> mMigrations =

new SparseArrayCompat<>();

比如(4,6)和(4,8)那麼就是append一個key是4的,然後裡邊包含的key是6和8的sparsearray

簡單分析下,我們隻說更新,不說降級。

如下邏輯,從4更新到8,那麼根據start=4,找到一個sparsearray,這裡包含2個key是6和8,它是倒着來,找到第一個小于等于endVersion也就是8了,這個剛剛好就找一次。

然後分析下從8更新到11,有個(8,10)和(9,11)

先查8,找到一個end10,然後 從10開始查,結果10 沒找到,是以傳回一個null。因為傳回的null。再下邊那個方法可以看到,傳回null的話資料庫就被清空了。

private List findUpMigrationPath(List result, boolean upgrade,

int start, int end) {

final int searchDirection = upgrade ? -1 : 1;

while (upgrade ? start < end : start > end) {

SparseArrayCompat targetNodes = mMigrations.get(start);

if (targetNodes == null) {

return null;

}

// keys are ordered so we can start searching from one end of them.

final int size = targetNodes.size();

final int firstIndex;

final int lastIndex;

if (upgrade) {

firstIndex = size - 1;

lastIndex = -1;

} else {

firstIndex = 0;

lastIndex = size;

}

boolean found = false;

for (int i = firstIndex; i != lastIndex; i += searchDirection) {

final int targetVersion = targetNodes.keyAt(i);

final boolean shouldAddToPath;

if (upgrade) {

shouldAddToPath = targetVersion <= end && targetVersion > start;

} else {

shouldAddToPath = targetVersion >= end && targetVersion < start;

}

if (shouldAddToPath) {

result.add(targetNodes.valueAt(i));

start = targetVersion;

found = true;

break;

}

}

if (!found) {

return null;

}

}

return result;

}

這裡是調用的方法

public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {

boolean migrated = false;

if (mConfiguration != null) {

List migrations = mConfiguration.migrationContainer.findMigrationPath(

oldVersion, newVersion);

if (migrations != null) {

for (Migration migration : migrations) {

migration.migrate(db);

}

mDelegate.validateMigration(db);

updateIdentity(db);

migrated = true;

}

}

if (!migrated) {//如果沒找到List migrations,那麼就會清空表的

if (mConfiguration != null && !mConfiguration.isMigrationRequiredFrom(oldVersion)) {

mDelegate.dropAllTables(db);

mDelegate.createAllTables(db);

} else {

throw new IllegalStateException("A migration from " + oldVersion + " to "

+ newVersion + " was required but not found. Please provide the "

+ "necessary Migration path via "

+ "RoomDatabase.Builder.addMigration(Migration ...) or allow for "

+ "destructive migrations via one of the "

+ "RoomDatabase.Builder.fallbackToDestructiveMigration* methods.");

}

}

}

添加一個新的entity

錯誤日志,說這個實體類沒有加到database,啥鬼啊,搞不懂。後來才查到,注解那裡要添加的,忘了

@Database(entities = {Userjava.class,HourseJava.class},version =1)

public abstract class JavaDatabase extends RoomDatabase

E:\androidStudio\RoomTest\app\src\main\java\com\charliesong\roomtest\room\HourseJavaDao.java:9:

錯誤: com.charliesong.roomtest.room.HourseJavaDao

is part of com.charliesong.roomtest.room.JavaDatabase

but this entity is not in the database. Maybe you forgot to add com.charliesong.roomtest.room.HourseJava

to the entities section of the @Database?

void insertHourse(HourseJava ...hourseJavas);

^

添加新的entity步驟

需要在database的注解字段entities裡添加,完事需要更新版本号,記得添加migration,

比如你從2更新到3,那麼必須添加一個Migration(2,3),否則如果沒找到這玩意,資料庫就被清空了。還得自己在migration方法裡添加create table的指令,建立新表,要不會報錯的。

後記

room這東西是編譯的時候生成相關的類的,databinding也是,有時候是room這邊有問題,導緻databinding生成失敗,結果你看錯誤資訊都是databinding的。

注解學習

錯誤記錄

Cannot figure out how to save this field into database. You can consider adding a type converter for it.

原因,實體類裡有個自定義對象,不認識

public DataType dataType;//這玩意就是個枚舉類型

解決辦法

@TypeConverters(DataTypeConverter.class)//注解用哪個類來解析存儲資料

public DataType dataType;

import androidx.room.TypeConverter;

//寫一個轉化類,其實就是需要兩個方法可以互相轉化這個自定義類,轉化為基本資料類型,也就是可以存入資料庫的

public class DataTypeConverter{

@TypeConverter

public OneTouchBean.DataType toDataType(int index){

return OneTouchBean.DataType.values()[index];

}

@TypeConverter

public int toIndex(OneTouchBean.DataType dataType){

return dataType.ordinal();

}

}

這個注解@TypeConverters可以寫在要解析的類上邊,也可以寫在@Database注解的類裡,可以參考workManager相關的包裡邊有,如下代碼

@Database(entities = {

Dependency.class,

WorkSpec.class,

WorkTag.class,

SystemIdInfo.class,

WorkName.class},

version = 6)

@TypeConverters(value = {Data.class, WorkTypeConverters.class})

public abstract class WorkDatabase extends RoomDatabase {

kotlin寫在屬性上,還是報錯,必須寫在類上邊

java.lang.IllegalStateException: getDatabase called recursively

代碼如下

在onCreate裡,資料庫建立成功以後,執行插入一些預設的資料操作,出錯了

public abstract class LocalDatabase extends RoomDatabase {

private static LocalDatabase database;

public static LocalDatabase instance() {

if (database == null) {

synchronized (LocalDatabase.class) {

if (database == null) {

database = Room.databaseBuilder(MainApplication.getCurrentInstance(), LocalDatabase.class, "test").

addCallback(new Callback() {

@Override

public void onCreate(@NonNull SupportSQLiteDatabase db) {

super.onCreate(db);

System.out.println("onCreate===========" + db.getVersion() + "===" + db.getPath());

resetOneTouchData();//這裡的代碼挂了

}

//方法如下,就是插入資料,沒别的

private static void resetOneTouchData(){

database.oneTouchDao().insertAll();}

根源就在于db還沒關,我們又開了個db

簡單的修改,把這個插入資料的操作放到一個新的線程中,讓目前的db結束掉就好了。

Entity的實體類,有多個構造方法的時候會出錯

錯誤: Room cannot pick a constructor since multiple constructors are suitable. Try to annotate unwanted constructors with @Ignore.

按照提示注解掉多餘的,留下一個即可

注意

當初看demo也沒看到關閉資料庫,是以我也沒關,當然,關不關好像對資料沒啥影響

如果不關,那麼可以看到多了2個臨時檔案,隻有關閉的時候臨時檔案的資料才會寫入db裡,否則db檔案可能是空的,當然資料沒有丢失,你打開還在,不過是在臨時檔案裡而已。

public static void closeDb(){

if(javaDatabase!=null){

javaDatabase.close();

javaDatabase=null;

}

}

android room 簡書,android Room庫使用問題

image.png

資料庫更新,表添加一列問題多多

需求:給表加一列如下的字段

private long placeUID =-1;

完事我就加了

public void migrate(@NonNull SupportSQLiteDatabase database) {

final String addColumn="alter table XXXBean add placeUID long default -1";

database.execSQL(addColumn);

}

結果挂了,異常提示expected 和found的不一樣

Expected:

placeUID = Column {

name = 'placeUID', type = 'INTEGER', affinity = '3', notNull = true, primaryKeyPosition = 0, defaultValue = 'null'

},

Found:

placeUID = Column {

name = 'placeUID', type = 'Long', affinity = '1', notNull = false, primaryKeyPosition = 0, defaultValue = '-1'

},

我實體類裡定義的是long,實際上room是當做int來處理的,是以,我隻好把sql語句改了,類型改成INTEGER 了

final String addColumn="alter table XXXBean add placeUID INTEGER default -1";

完事還是挂,再對比提示資訊,還有一個不一樣的notNull = true

繼續修改

final String addColumn="alter table XXXBean add placeUID INTEGER default -1";

終于完事了,不挂了

後記

沒事可以看看work這個庫,這裡也用到了room,看看人家咋存的

android room 簡書,android Room庫使用問題

image.png

常用的簡單的增删改查

@Dao

public interface MyPlaceDao {

@Insert

void insertAll(MyPlaceBean... myPlaceBeans);

@Insert

long insertAll(MyPlaceBean myPlaceBeans);

@Query("select * from MyPlaceBean order by uid desc")

LiveData> getMyPlacesFromSync();

@Query("select * from myplacebean where locationData = :json")

MyPlaceBean getMyPlaceBeanByLocationJson(String json);

//COLLATE NOCASE 忽略大小寫

@Query("select * from myplacebean where changedName = :name COLLATE NOCASE and uid != :uid")

List getMyPlaceBeanByChangedName(String name,long uid);

@Delete

int delete(MyPlaceBean ...myPlaceBean);//參數可以是數組,集合,傳回的是删除成功的條數

@Delete

int delete(List myPlaceBean);//參數可以是數組,集合,傳回的是删除成功的條數

@Update

int update(MyPlaceBean... myPlaceBeans);//參數可以是數組,集合,傳回的是update成功的條數

@Update

int update(List myPlaceBeans);//參數可以是數組,集合,傳回的是update成功的條數

@Query("update myplacebean set changedName = :name where uid = :uid")

int updateName(String name, long uid);

}

需要注意下,這裡傳回的隻能是List,而不能是ArrayList,否則編譯就挂了

LiveData> getMyPlacesFromSync();

kotlin需要注意的地方

TypeConverters注解是寫在class上了,

下邊這個自增的主鍵要寫到構造方法裡,否則,它沒法自動生成id,預設就是個0

@PrimaryKey(autoGenerate = true)

var id:Int=0

@Entity

@TypeConverters(DataTypeConverter::class)

data class Task(

val name: String,

val deadline: String,

val priority: TaskPriority =TaskPriority.MEDIUM ,

var completed: Boolean = false,

@PrimaryKey(autoGenerate = true)

var id:Int=0

)

比如這樣寫,你會發現資料庫裡id都是0

data class Task(

val name: String,

val deadline: String,

val priority: TaskPriority =TaskPriority.MEDIUM ,

var completed: Boolean = false

){

@PrimaryKey(autoGenerate = true)

var id:Int=0

}