Android Room Database 学习

Posted microhex

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Room Database 学习相关的知识,希望对你有一定的参考价值。

Room数据库学习

初衷

由于接手的项目中,看到别人使用的是android Jetpack下的Android Room数据库,由于以前也只是接触过GreenDao和自己写的sqlite数据库,所以就学习了一下,做一个笔记记录一下。这是我以前学习的greendao笔记,可以横向比较一下:https://blog.csdn.net/u013762572/article/details/78133463

概览

配置

现在有两个版本的 一个是andoirdx版的,一个是android版的。

我们可以上这个网站搜索需要的版本序号:
https://mvnrepository.com/

我贴一下当前我自己所有使用的版本:

implementation "android.arch.persistence.room:runtime:1.1.0"
annotationProcessor "android.arch.persistence.room:compiler:1.1.0"

另外也贴一下androidx版本的吧:

implementation "androidx.room:room-runtime:2.1.0-alpha07"
annotationProcessor "androidx.room:room-compiler:2.1.0-alpha07" 

另外多说一句,就个人而言,androidx包一定会成为主流,来替代各种版本的support包,所以如果你还没有使用上androidx,那么请早些使用。

如果你的代码中包含了kotlin,那么需要下面的配置:
build.gradle下面需要添加plguin

apply plugin: 'kotlin-kapt'


dependencies 
    implementation 'androidx.room:room-runtime:2.0.0'
	
	//使用了kotlin之后,annotationProcessor需要kapt的实现,以配合kotlin-kapt插件使用
    kapt 'androidx.room:room-compiler:2.0.0'

    implementation "androidx.room:room-ktx:2.1.0-alpha05"
 

当然以上版本,你可以在上面的网站上找到最新的版本,因为版本更新比较快,一段时间之后将会存在更新的,更稳定的版本

定义data类

我们需要实例化Class类,需要加上@Entity注解,其他用到的注解,将会一一说明,大致代码如下:

@Entity(tableName = "t_user")
public class User 

    public User()  

    @PrimaryKey(autoGenerate = true)
    public long id ;

    @ColumnInfo(name = "user_name")
    public String userName;

    public String address ;

    @ColumnInfo(name = "age", typeAffinity = ColumnInfo.INTEGER)
    public int age ;

    public boolean sex ;

    @Ignore
    public User boyFriend;


@Entity 实体类的标注,将会在数据库映射成对应的表结构,tableName不写,将会是实体类默认
@PrimaryKey  主键 
@ColumnInfo  字段属性对应列的关系 没有该注解时,属性名将一一对应字段名
@Ignore  属性值将不会序列化为字段,与@java.beans.Transient意义一致

定义UserDao

定义查询接口,需要的查询的方法都需要提前定义,如下:

@Dao
public interface UserDao 

    @Query("SELECT * FROM t_user")
    List<User> queryAllUserInfo();

    @Query("SELECT * FROM t_user WHERE id = :id")
    User getUserById(long id);

    @Insert(onConflict = OnConflictStrategy.REPLACE )
    long[] insertUser(User... users);

    @Update
    void updateUser(User user);

    @Delete
    void deleteUser(User user);

@dao 标记接口为数据访问对象,将会自动生成其实现类来查询数据
@Query 查询接口
@Insert 插入接口
@Update更新接口
@Delete删除接口	

定义抽象数据库

定义需要访问的抽象数据库,定义数据库中需要存储的data类,以及数据库的版本信息,也是一成不变,基本模式如下,该数据库对象必须是抽象的,而且需要继承RoomDatabase

@Database(entities = User.class, version = 2)
public abstract class AbstractUserDataBase extends RoomDatabase 
    public  abstract UserDao getUserDao();


@Database标注为一个RoomDatabase  里面有多少张表,entities里面也必须都填写

项目build 自动生成文件

如果一切都正确,将生成UserDao和AbstractUserDataBase的实现类,如下图所示:

使用(CRUD)

获取AbstractUserDataBase的实现类:

AbstractUserDataBase userDataBase=  
Room.databaseBuilder(getContext(),AbstractUserDataBase.class,"userDataBase").build()

插入数据:

User u = new User();
u.userName= "Tom";
u.age = 19;
u.address = "shanghai"; 

userDataBase.getUserDao().insertUser(u)

删除数据:在这里插入代码片

User u = new User();
u.id = 4L;

useDaoBase.getUserDao().deleteUser(u);

查询数据:


List<User> listList = userDaoBase.getUserDao().queryAllUserInfo();

以上是比较基本的操作,就不多说了。主要说一下需要注意的点,在我们使用更新的时候,如使用下面的方法:

@Update
fun updatePerson(p:Person)

我们更新的时候,如果这么写:

val p = Person(id = 1,name = "tom",age = 0)
userDaoBase.getUserDao().updatePerson(p)

Person这条记录将会被全局更新,意思就是它所有的字段都会被更新掉,这个是比较尴尬的,也是和我们平时后端使用数据库的想法是不一样的,一般的想法都是我更新时设置了什么就更新什么,但是Room比较蛋疼,会一下子更新全部。那么如果我们需要非要更新部分数据呢?那只能用@Query了。如下,只更新Person对象的userName字段:


@Query("update person set user_name = (:userName) where id = (:Id)")
fun updatePersonNameById(userName:String, id:Int) : Int

然后就可以写代码更新了。

表映射关系

因为Room数据库是Android Jetpack.组件之一,拥有良好的性能是它考虑的重点,速度和内存一直是它考虑的重点。为了达到这些要求,它设计的时候禁止了对象之间的引用。对于对象之间的引用,我们可以理解为(一对一,一对多,多对多)。Room是对sqlite的抽象,而sqlite本身就是一个关系型数据库,因此Room为了在使用过程中,避免查询一个数据,就需要查询多个其他多个属性的问题,就定义了独有的解决方案,个人感觉这个方案并没有想象中好,相反也许会增加使用过程中不必要的麻烦。对于没有想象中那么好,稍后我会说一下自己的想法。

使用外键【foreignKeys】

先来个例子,上面已经有了一个User对象的例子,还需要重新创建一个Book,以此来营造one User to mapper Many Books的任务,对于Book,实现如下:

@Entity(foreignKeys = [ForeignKey(
    entity = User::class,
    parentColumns = arrayOf("id"),
    childColumns = arrayOf("user_id"),
    onDelete = ForeignKey.NO_ACTION,
    onUpdate = ForeignKey.NO_ACTION
)])

data class Book (

    @PrimaryKey(autoGenerate = true)
    val bookId : Int,

    val title: String?,

    @ColumnInfo(name = "user_id")
    val userId:Int
)

先不看代码,解释一下:

Book对象有三个属性,资格自增Id、一个书名(title)、一个外键(userId)
@entity中foreignKeys 属性的含义,entity是需要连接到哪个实体类,对应的parentColumns 就是需要链接的实体类的标识符(一般都为主键),数据库中也称之为主表, childColumns 就是本实体类中的字段属性,数据库中也称之为从表。
对于Ondelete、OnUpdate中对应的ACTION,这可能是ROOM做得比较优秀的点了,具体的ACTION有以下几个:


`NO_ACTION`: 主表更新,从表什么都不做。
`RESTRICT`:字面意思是限制或者约束,意思就是如果你想先修改或者删除主表的数据,但是这个主表是从表的外键,那么对不起,这种行为或者操作是不被允许的,是禁止的。如果真的要删除数据,那么请从从表开始修改或者删除,先不要拿主表开刀.
`SET_NULL`: 对应上面修改或者删除主表时的操作,此时操作被允许,只是从表中对应的外键都被设置为NULL
`SET_DEFAULT`:对应上面修改或者删除主表时的操作,此时操作被允许,只是从表中对应的外键都被重置,该是什么默认值,就是什么默认值
CASCADE: 这个需要分开说,当主表数据`修改(update)`时,从表中数据也修改,来重新满足外键关系;当主表数据删除(delete)时,从表数据也删除。

总体来说,这种外键我们一般也是用得比较多,也懒得讲了,平时该怎么用怎么用,而Room就是多了几个选项,可以在处理外键问题上来的更加容易一下。

@Embedded注解

在应用中,存在这样一个场景,一个Person对象,其中包含了一个地址Address对象,Address对象中包含了地址信息,基本类型结构为:


data class Address(

    val street:String? ,

    val state:String?,

    val city:String?
) 
    
    override fun toString(): String 
        return "Address(street=$street, state=$state, city=$city)"
    

现在Person对象需要包含一个Addres对象:

data class Person(
    @PrimaryKey(autoGenerate = true) val id : Int,
    val name : String?,
    val age: Int,
    val address: Address?
) 

但是上面已经明确说明,Room中不能搞对象引用,不然真的没法存。为了解决这个问题,Room使用了@Embedded,而Embedded的中文意思就是嵌入式,就意味嵌入到Person对象中:

@Entity(tableName = "t_person")
data class Person(
    @PrimaryKey(autoGenerate = true) val id : Int,
    val name : String?,
    val age: Int,

    @Embedded val address: Address?
) 

那么具体的表结构最终生成什么样子的呢?看一下截图就知道了:

可以看到,Room直接把Address的字段完全搬到了Person的后面,这个就很尴尬了,我在做的过程中,出现了这样一下问题:

  1. AddressPerson中不能有同名的字段
  2. Address对象不再是数据库中对象,它只是一个简单的class bean对象了,这个需要理解一下,不然很容易搞错。

Many2Many问题

总有地方存在多对多的数据结构,那么这种问题,Room该怎么处理呢?其实这一点,我感觉Room禁止对象引用还是没有做到彻底,因为在我看来,它最终还是搞了对象引用。在Many2Many的问题上,Room同greendao一样,使用了中间表的概念。
引用官方的例子,一首歌Song和一个播放列表Playlist是多对多的关系:一首歌可以在多个播放列表中;一个播放列表可以存在多首歌。大致的class结构如下:
歌曲Song结构

@Entity
data class Song(
    @PrimaryKey(autoGenerate = true)val id : Int,
    val name :String,
    val artistName:String
 )

播放列表Playlist结构

@Entity
data class Playlist(
    @PrimaryKey(autoGenerate = true) val id : Int,
    val name:String?,
    val description:String?
)

那么中间表为:

@Entity(tableName = "play_song_join",
        primaryKeys = ["play_list_id","song_id"],
        foreignKeys = [
                ForeignKey(entity = Playlist::class,
                            parentColumns = ["id"],
                            childColumns = ["play_list_id"]),

                ForeignKey(entity = Song::class,
                          parentColumns = ["id"],
                          childColumns = ["song_id"])
        ]
    )
data class PlaySongJoin(
    val play_list_id: Int,
    val song_id : Int
)

结构很清晰,中间表存储两个对象的主键,最后一一对应。在查询时,此时就需要使用SQL代码了:

    @Query("""
        SELECT * from playlist
        INNER JOIN play_song_join
        ON playlist.id = play_song_join.play_list_id
        WHERE play_song_join.song_id = (:songId)
    """)
    fun getPlayListsForSongs(songId:Int) : List<Playlist>

    @Query("""
        SELECT * FROM song
        INNER JOIN play_song_join
        ON song.id = play_song_join.song_id
        WHERE play_song_join.play_list_id = (:playListId)
    """)
    fun getSonsForPlayList(playListId:Int) : List<Song>
    

使用内连接来查询数据,还是对数据库SQL语句有些要求。

数据视图

这个平时用的比较少,但是还是聊一下这东西。而且也是2.1.0版本之后的新东西。我们上面定义过了UserBook两个实体类,如果有个需要同时需要User信息和Book信息,按照一般的做法,我们需要查询两遍,一遍查User,一遍查Book,效率有些低下,数据视图就是解决这个问题。首先我们定义下UserBookDetail(此时我们假设User和Book是一一对应关系):

public class UserBookDetail
    public long id ;
    public String userName;
    public String address ;
   	public int age ;
    public boolean sex ;

	//书ID
    public long bookId;
    //书名
    public String title ;

则使用数据视图,又需要重新写SQL语句了:


@DatabaseView(" 
SELECT t.id, 
       t.user_name,
       t.address,
       t.age,
       t.sex,
       b.book_id,
       b.title
	   from user t inner join book b on t.id = b.user_id ")
public class UserBookDetail
    public long id ;
    public String userName;
    public String address ;
   	public int age ;
    public boolean sex ;

	//书ID
    public long bookId;
    //书名
    public String title ;

这个用的不多,我也只是记录一下,具体的可以参考官方文档:)

使用总结

使用Room一段时间之后,有几点需要注意一下:

  1. 所有的查找、修改、删除、增加等操作都需要放在子线程,这个是必须的,不然就一定报错。
  2. 通过上面的数据我们看出,数据之间的操作,都是需要id的,而我们查找的这个id不是很方便获取,即使我们insert了这个对象,返回的也只是数据库影响的列数,对象中Id也没有及时修改过来,这个和greendao相比,还是有些差距的。
  3. 速度还是蛮快的,其他的,暂时还没有发现什么毛病,这是每次写个查询,都需要build一次还是蛮麻烦的。

学习代码为:
https://github.com/Microhx/studyCodes/tree/master/room

以上是关于Android Room Database 学习的主要内容,如果未能解决你的问题,请参考以下文章

Android Room Database:如何处理实体中的 Arraylist?

获取 android 房间数据库 java.lang.IllegalArgumentException 的以下异常:@androidx.room.Database 未定义元素视图()

添加1到多关系android Room Database?

Android Room Database,检索输入的最新记录的特定值

Android Room Database:如何嵌入多个实体

Android Room Database 执行批量更新的正确语法是啥?