android Room库使用问题
资料
https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/index.html?index=..%2F..index#0
https://github.com/android/sunflower
https://www.jianshu.com/p/2aa8bb141d6e
学习过程记录
照着各种文章写,简单的实现都一样,可跑起来总是挂
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写,然后就可以了。
如下
后记
后来发现,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'
另外添加如下注释的地方下边的代码,具体可参考https://blog.csdn.net/hexingen/article/details/78725958
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;@Entitypublic 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;@Daopublic 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)
顺道记录些其他问题
https://blog.csdn.net/hexingen/article/details/78725958
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 { ...}
方法2
配置下文件保存的位置
arguments 这个写法可能会引起其他插件异常的,需要注意,比如Hilt库
下边有解决办法
https://stackoverflow.com/questions/62887817/expected-hiltandroidapp-to-have-a-value-did-you-forget-to-apply-the-gradle-plu
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文件里可以看到我们定义的表以及其包含的字段,主键等信息
image.png
版本升级
https://www.jianshu.com/p/df48fb35a1fe
知道升级要添加的方法,可是咋复制老的数据库不记得了,所以还是找别人写的看好了。最讨厌数据库了,2个表就晕了。
打印下可以看到,第一次操作数据库的时候,先create,再open
System.out.println("onCreate==========="+db.getVersion()+"==="+db.getPath());
onCreate===========0===/data/user/0/com.charliesong.demo0327/databases/testonOpen===========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============211-15 09:15:27.556 com.charliesong.demo0327 I/System.out: migrate34============211-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的。
注解学习
https://blog.csdn.net/hubinqiang/article/details/73012353
错误记录
- 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; } }
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,看看人家咋存的
image.png
常用的简单的增删改查
@Daopublic 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}
更多相关文章
- ViewPager添加动画效果(一行代码)
- 给android添加系统属性
- Android调用百度地图API实现――实时定位代码
- Android(安卓)sdk更新代理配置
- android 下Excel操作
- (Android)调用百度地图api之添加覆盖物
- Android(安卓)Studio3.2 Butter Knife配置填坑
- android_常用UI控件_02_EditText_01添加图片到edittext中
- Ubuntu 12.04 Desktop 版本编译 Android(安卓)4.0.4 出错解决