本篇单纯讲解Room源码,和前后篇章没有关系。
当前Room注解大全是基于Room2.4版本代码逻辑得来,如果有问题欢迎给予指正。当前Room源码学习目的:(1)主要原因:学习jetpack代码;(2)次要原因:网上看了很多相关Room注解使用,非常肤浅(可能个人眼界有限,没有找到真正比较全面的注解),所以想自己看代码去全面理解一下room注解;
之前有了dagger2(基于2.38.1版本)注解理解基础,所以这里相对来说理解起来较简单,也是相当来说而已,代码看起来还是头疼得很!!!
理解Room注解前提条件:
必须!!!是必须理解sql数据库相关操作!!!如果想全面理解当前Room,那么不但对数据库基本操作熟悉,对其他如视图、索引也要有一定了解,这样才会事半功倍。
以下讲解围绕数据库创建和数据库操作两部分讲解:
还会细分,创建数据库有表、视图、关系、表字段等;操作数据库又分为对数据库增删改查,还有事务等。
下面根据数据库创建和操作来对Room注解一一解析,并且按照常用在前,不常用在后原则慢慢品味源码。
这里是我自己命名的一些词语。相当于一个给下面的Room注解有些名词进行解释,这里先不需要看,等看到下面具体的名词的时候回到这里来看。
表字段:表或视图的字段;
xx节点:表示使用@Xx注解修饰的节点,e.g.@Entity修饰的节点表示entity节点;
fts节点:@Fts3或@Fts4修饰的 entity节点;
pojo节点:下面有一个篇章专门介绍pojo对象的生成,表示能生成Pojo对象的节点;
pojo节点有效方法:非private修饰 && 非abstract修饰 && 非@Ignore修饰;
pojo节点setter方法:有且仅有一个参数,返回类型是void;
pojo节点getter方法:pojo节点有效方法中,无参,返回类型不是void;
表有效字段:pojo节点中所有字段(包括父级类中的字段),并且该字段满足 : 没有被@Ignore修饰 && 不是static修饰 && (没有使用transient修饰 || (使用了@ColumnInfo或@Embedded或Relation修饰));
表常规字段:表有效字段中使用@ColumnInfo修饰 或 没有使用transient修饰的表字段;如果当前字段表示的节点使用@Entity修饰,那么不存在于@Entity#ignoredColumns中;
表关系字段:表有效字段中使用@Relation修饰;
表嵌入字段:表有效字段中使用@Embedded修饰;如果当前字段表示的节点使用@Entity修饰,那么不存在于@Entity#ignoredColumns中;
嵌入表:表有效字段中使用@Embedded修饰的字段对象生成的表;
关系表:表有效字段中使用@Relation修饰的字段对象生成的表;
感觉名字是不是有点大!!!可能有那么一点,但是我把源码中关于Room注解的逻辑全部提炼出来也就那么回事儿。
来来回回一遍遍去理解,花的都是心血啊,如果对读者有帮助,必须给个star。
一切都是从@Database注解说起,使用当前注解拉开Room注解篇章,从此踏上不归路,呸呸呸…从此走上人生癫疯!!!嗯,癫疯!
本篇核心规则之一:必须先理解,而且这么去写。
情况一:数据库创建过程中生成Pojo对象:
entity节点:@Entity修饰的节点;
fts节点:@Fts3或@Fts3修饰的节点同时使用@Entity修饰;
databaseView节点:@DatabaseView修饰的节点;
embedded修饰的节点类型:@Embedded修饰的节点类型,该表示的对象节点;
情况二:数据库操作过程中创建Pojo对象:
pojo节点生成pojo对象过程中,有可能pojo节点中的子节点又生成一个子pojo对象,正确情况下(肯定存在非正常情况,感兴趣自己看源码)不能出现pojo对象循环引用,e.g.该子pojo节点类型就是该父级pojo节点类型;
正常情况下,pojo节点的所有方法不允许使用@PrimaryKey, @ColumnInfo,@Embedded, @Relation修饰;但是如果pojo节点同时使用@AutoValue修饰,规则如下:
如果pojo节点是entity节点,那 @Entity#ignoredColumns属性表示被忽略的字段,被忽略的字段只允许是表常规字段和表嵌入字段;
表常规字段和嵌入表常规字段不允许重复;
pojo节点构造函数,筛选条件:
(1)pojo节点(没有使用@AutoValue修饰)构造函数条件:当前构造函数没有被@Ignore修饰 || 当前构造函数不是private修饰;如果pojo节点被@AutoValue修饰,那么 : 方法没有被@Ignore修饰 && 方法没有被private修饰 && 方法是static修饰 && 方法返回类型是当前类类型;
(2)构造函数参数筛选:
① 如果pojo节点的构造函数参数是当前pojo生成的表常规字段、表嵌入字段或表关系字段,则没有问题;
② 如果不满足条件①,那么:要么表常规字段、表嵌入字段或表关系字段都不存在;要么有且仅有一个表字段(常规、关系或嵌入中的一个),也就说该情况下允许构造函数参数不匹配表字段情况;否则都报错;
(3)如果经过以上筛选存在一个构造函数,并且该构造函数要有参数;如果存在多个构造函数,是kotlin语言直接返回主构造函数,不存在主构造函数,存在参数为空的构造函数则警告;多个构造函数筛选出第一个即可;
除了pojo节点常规字段,还有嵌入表的常规字段、关系表的常规字段和嵌入表中的关系表的常规字段。
(1)如果pojo节点中的变量存在于构造函数中,那么不需要校验setter方法;否则按照如下顺序一步步校验;
(2)变量如果是public修饰,不需要校验setter和getter方法;
(3)如果setter和getter方法不是public修饰,那么当前变量不是private修饰,也表示校验成功;
(4)setter和getter方法分别最多只能存在一个;如果存在多个报错;
(5)除了以上情况,其他情况报错;
element:用于创建Pojo的节点;如果节点同时使用@AutovAlue表示新生成的节点:Auto_原先节点;
type:用于创建Pojo的节点类型;如果节点同时使用@AutovAlue表示新生成的节点类型:Auto_原先节点类型;
fields:表常规字段 + 嵌入表常规字段;
embeddedFields:pojo节点中的embedded节点生成的对象;
relations:pojo节点中的relation节点生成的对象;
constructor:pojo节点构造函数。
使用@Database注解的节点,表示生成一个数据库文件。
一个项目中@Database注解可以存着多个,表示生成多个数据库文件;
@Database#entities属性表示在当前数据库文件中创建表信息:必须存在,属性值类型对象,必须使用@Entity修饰;
@Database#views表示创建视图信息:属性值类型对象必须使用@DatabaseView修饰;
@Database修饰的类必须继承androidx.room.RoomDatabase
类;
一定要确保每个数据库文件中的@Database#entities和@Database#views中的表和视图名称不重复;
@Database修饰的类必须是abstract抽象类,其abstract(非RoomDatabase继承过来的)抽象方法:
@Entity注解修饰的类表示数据库中的表信息。
@Entity#inheritSuperIndices:默认false;如果为true,表示当前@Entity修饰的类的父类如果也使用了@Entity修饰,那么继承其父类的@Entity#indices索引;
@Entity#foreignKeys:表外键;
entity节点的所有方法不允许使用@PrimaryKey、 @ColumnInfo、@Embedded和@Relation修饰;但是:如果entity节点同时使用了@AutoValue修饰,那么:
(1)entity节点下的方法允许使用@PrimaryKey、 @ColumnInfo、@Embedded和@Relation修饰;
(2)entity节点下的无参抽象方法(该类只允许存在无参抽象方法)如果使用了androidx.room包下的注解,那么最好无参抽象方法也是用@CopyAnnotations修饰,否则报警告;
@Entity#ignoredColumns表示忽略的表字段;哪些表字段可以被忽略:①表常规字段;②表嵌入字段;
@Entity#primaryKeys表示表的主键;
@Entity修饰的节点可以生成pojo对象,所以需要按照参照生成pojo对象规则;
@Entity修饰的类中不允许出现@Relation修饰的有效字段;
entity节点同时使用@Fts3或@Fts4修饰的节点,称之为fts表。
fts表属性和规则如下:
@Fts3#tokenizer:FTS3表中使用的标记器;
@Fts3#tokenizerArgs:FTS3表中用于配置定义的标记器的可选参数;
@Fts4#tokenizer:FTS4表中使用的标记器;
@Fts4#tokenizerArgs:FTS4表中用于配置定义的标记器的可选参数;
@Fts4#contentEntity:FTS4表映射表的外部内容实体将用作FTS表的内容;
(1)当前属性必须存在,并且是使用@Entity注解修饰的类;
(2)fts表中除了rowid主键和languageId(@Fts4#languageId)字段,其他字段必须存在于@Fts4#contentEntity中属性对象生成的表常规字段或嵌入表常规字段中;
(3)@Fts4#contentEntity中的对象必须存在于@Database#entities中
@Fts4#matchInfo:Fts版本,这里只有FTS3和FTS4两个版本;
@Fts4#notIndexed:FTS4表上不会被创建索引的表字段;
@Fts4#order:FTS表的首选“rowid”顺序;
表名::@Entity#tableName属性值如果存在,则使用该属性值;否则使用@Entity修饰的类名;
fts表不允许创建索引,不允许使用外键
fts节点也可以生成pojo对象,所以fts节点必须满足pojo对象生成规则;
fts表不允许存在表关系字段(@Relation修饰的有效变量);
影子表名:
@DatabaseView注解修饰的类表示数据库中视图信息。
视图如何使用:
@Database#views;
@Relation修饰的表关系对象使用@DatabaseView;
@Relation#associateBy#value中的对象使用@DatabaseView,表示多对多关系,e.g.
@Relation(
parentColumn = "playlistId",
entity = Song::class,
entityColumn = "songId",
associateBy = @Junction(
value = PlaylistSongXRef::class,
parentColumn = "pId",
entityColumn = "sId")
)
注①:@Relation注解不能修饰@Entity修饰的类的有效字段;
注②:外键指向的必须是@Entity修饰的对象表;
视图属性和使用规则如下:
@DatabaseView#vlue属性表示select查询:当前属性必须存在,并且是正确的select语句;
@DatabaseView#viewName属性表示视图名称:如果当前属性不存在,则使用@DatabaseView修饰的节点名称作为视图名称;
视图名称不能使用"sqlite_"前缀;
视图节点可以用于床架pojo对象,所以必须满足生成pojo对象规则。
pojo节点中的变量用于生成表字段,有三种类型:表常规字段,表嵌入字段和表关系字段(看术语解释)。
表有效字段中使用@ColumnInfo修饰 或 没有使用transient修饰的表字段;如果当前字段表示的节点使用@Entity修饰,那么不存在于@Entity#ignoredColumns中。
@ColumnInfo注解属性:
@ColumnInfo#name作为表字段名称;
@ColumnInfo#typeAffinity表示表字段偏向类型;
@ColumnInfo#index表示表字段是否创建索引;默认false
@ColumnInfo#collate表示当前表字段排序规则;
@ColumnInfo#defaultValue表示当前表字段默认值;
表常规字段规则如下:
2.1 如果表字段类型是泛型,那么必须是实体泛型,例如List是正确的泛型格式,List< T>格式错误;
2.2 表字段类型,根据当前变量类型确定生成的表字段类型,如果不存在于:
(1) TEXT-文本类型:
① String类型;
(2) INTEGER-int类型:
① int、short、byte、long和char四种基本类型;
② Integer、Short、Byte和Long四种基本类型的包装类型;
③ boolean或Boolean,当前源码会自动将其转换成int类型,true使用1表示,false使用0表示,所以boolean和Boolean生成INTEGER类型表字段;
(3) REAL-长int类型:
① float和double基本类型;
② Float和Double基本类型的包装类型;
(4) BLOB-长文本类型:
① byte[]
② ByteBuffer类型
(5)使用@ColumnInfo修饰表常规字段,@ColumnInfo#typeAffinity表示当前表字段偏向类型,最好不设置。
① 设置了@ColumnInfo#typeAffinity,首先根据变量类型去匹配表字段类型,如果匹配成功,再根据匹配的表字段类型是否等于我们设置的表字段偏向类型,如果没有匹配上,则表示当前适配不成功,继续往下匹配;
② 没有设置@ColumnInfo#typeAffinity,就不会存在(5)①中的再次匹配偏向类型的情况;
(6)如果变量类型不属于以上四种类型,那么还会通过在@TypeConverters修饰的节点实现类型转换(自行查看下面类型转换篇章),最终也是转换成以上四种类型;
(7)以上条件都不满足,还有一种特例,变量类型是枚举或UUID类型,有可能(是有可能,这种情况基本不可能)也会生成对应的类型;
(8)以上条件都不满足,那么肯定会报错;
表嵌入字段,@Embedded修饰的字段。
@Embedded#prefix属性,表示当前@Embedded修饰的变量对象中的变量生成表字段的需要添加的前缀。
规则如下:
@Embedded修饰的有效字段类型必须是类或接口;
@Embedded修饰的有效字段类型,不能存在递归引用。e.g.@Entity修饰的节点和@Entity节点中@Embedded修饰的节点类型一致,则表示递归,肯定不被允许;
当前@Embedded修饰的节点类型会生成Pojo对象;
表关系字段,@Relation修饰的字段。
@Relation注解属性:
@Relation#entity属性:存放对象类型必须使用@Entity修饰或@DatabaseView修饰;表示当前表或视图关联的表或视图对象;
@Relation#parentColumn属性:当前表字段(一般是外键),用于关联@Relation#entity对象(一般关联的的是主键)信息;
@Relation#entityColumn属性:存在于@Relation#entity对象对象中的表字段(一般是主键),该表字段被@Relation#parentColumn(一般是外键)关联;
@Relation#associateBy属性:如果当前对象和@Relation#entity对象是多对多关系,则还需要使用当前属性关联;
@Relation#projection属性:@Relation#entity对象的表或视图中提取需要的字段,如果为空,表示提取表或视图中的全部字段。
@Junction注解属性:
@Relation(
parentColumn = "playlistId",
entity = Song::class,
entityColumn = "songId",
associateBy = @Junction(
value = PlaylistSongXRef::class,
parentColumn = "pId",
entityColumn = "sId")
)
val songs: List
--------------------- 分割线:上下是两个不同类------------------
@Entity(primaryKeys = {"pId", "sId"})
public class PlaylistSongXRef {
val pId: Int,
val sId: Int
}
@Junction#parentColumn属性:对应的是@Relation#parentColumn字段, 如果@Junction#parentColumn没有设置,那么使用@Relation#parentColumn中的字段;
@Junction#entityColumn属性:对应@Relation#entityColumn字段,如果@Junction#entityColumn没有设置,那么使用@Relation#entityColumn中的属性;
@Junction#value属性:必须使用@Entity 或 @DatabaseView修饰的类,该类表示用于存储当前表主键和受关联表主键;
规则如下:
@Relation#parentColumn的属性值必须存在,而且必须包含在当前表常规字段或当前表的嵌入表常规字段中;
表关系字段类型,如果是集合,那么当前集合只允许是List或Set集合,并且只允许List< T>,List
如果表关系类型是集合,T作为返回类型校验;否则返回类型校验直接校验表关系类型;返回类型是实际类型,e.g.String可以,?或者 T不可以;
@Relation#entity中的属性必须是类或接口,作为entity关系节点;如果当前@Relation#entity属性不存在,使用当前@Relation修饰的节点作为entity关系节点;
@Relation修饰的有效字段类型,不能存在递归引用。e.g.pojo节点和pojo节点中@Relation修饰的节点类型一致,则表示递归,肯定不被允许;
@Relation#entityColumn必须存在,并且该字段存在于entity关系节点类型中的所有有效字段中;
如果关系对象是多对多关系,还需要使用@Junction注解,在@Relation#associateBy属性中设置。规则如下:
@Relation#associateBy的属性@Junction,@Junction#value的属性值类型必须是@Entity 或 @DatabaseView修饰;
parentColunm属性:在@Relation#associateBy属性中,如果@Junction#parentColumn存在,使用当前字段作为连接父级表的列属性;否则使用@Relation#parentColunm;
entityColumn属性:在@Relation#associateBy属性中,如果@Junction#entityColumn存在,使用当前字段作为连接实体表的列属性;否则使用@Relation#entityColumn;
parentColunm属性或entityColumn属性校验:
如何使用索引:
@ColumnInfo#index = true,表示当前表常规字段创建索引;
@Entity#indices,表示当前表中创建索引的表字段;
@Entity#inheritSuperIndices = true,并且@Entity修饰的父类也是用了@Entity修饰,其#indices属性集合;
其中,2和3使用的是@Index注解。
@Index注解属性:
索引名称:@Index#name属性值用于表示新建的索引名称;@Index#name属性值不存在,则使用 index_表名_表字段
;
@Index#value属性值表示索引字段;
@Index#orders属性值表示表字段在当前索引中的排序;
如果表中存在索引,那么索引规则如下::
一个表中新建的索引名称只能出现一次;
嵌入表中最好不要存在使用索引,否则会报警告,表示当前索引无效;
使用主键方式有两个:
@PrimaryKey注解属性:
@PrimaryKey#autoGenerate属性值:表示是否允许自动生成当前主键字段,默认是false;
主键使用规则如下:
表主键必须存在;主键有且仅有一个,但是可以由多个表字段组成;
如果@PrimaryKey#autoGenerate = true,那么主键字段不是int类型,必须使用@NonNull修饰,表示不允许null值;
主键默认是txt类型,如果主键是自动生成(@PrimaryKey#autoGenerate= true)的表字段 ,那么当前表字段必须是int类型;
只能通过@Entity#foreignKeys中的属性值来设置@ForeignKey外键;
@ForeignKey注解属性和规则如下:
@Entity#foreignKeys属性表示当前表外键;
@ForeignKey#entity属性必须存在,表示当前外键指向的表;
@ForeignKey#parentColumns属性表示当前表外键字段指向的外键表字段,必须存在与@ForeignKey#entity对象中;
@ForeignKey#childColumns属性,表示当前表外键字段,该字段指向@ForeignKey#entity对象的表字段(一般是主键);必须存在
@ForeignKey#deferred属性,如果为true表示当前外键约束存在于事务中,事务全部结束才会生效;
@ForeignKey#onDelete,@ForeignKey#onUpdate表示外键删除还是更新,有以下几种状态
表外键字段指向外键表字段:该外键表字段要么是主键,要么创建了唯一性索引;否则会报警告;
外键指向的表必须存在于@DatabaseView#entities中;
表示是否跳过数据库版本校验。规则如下:
如果@SkipQueryVerification和@Database同时使用,表示当前数据库版本不做校验;
如果@SkipQueryVerification和@DatabaseView,则仅仅表示不对当前@DatabaseView#vlue的sql校验;
同理,如果@SkipQueryVerification和@Insert(@Delete、@Query或@Update)一起使用,不对当前@Insert#value的sql校验;
@RawQuery修饰的方法无论有没有使用@SkipQueryVerification都不会去做当前方法的sql校验;
@TypeConverters修饰的注解实现类型转换,转换成表字段支持的类型。
当前表常规字段类型不支持boolean(或Boolean)类型的,但是我们却可以正常使用boolean或Boolean:因为Room系统为我们内部做了类型转换,boolean变量转换成表字段时,boolean = true转换成int = 1;boolean = false转换成int = 0;从表字段中提取数据时,同理,int转换成boolean。
先给个案例:
@Database(entities = News.class)
class Database extends RoomDatabase{
xxx;
}
实体类:
@Entity
@TypeConverters(ThumbConverter::class)
data class News(
@PrimaryKey
var row: String,
var title: String = "",
var type: Int = 2,
val thumb: List?,
var content_time: String? = "",
var source: String? = "",
var hot: Int = 0
)
类型转换器:@TypeConverter的两个方法是成对出现的,方法名称可以任意命名,重点在入参和出参类型,必须是需要转换的类型和最终转换后的类型。
class ThumbConverter {
//从表字段中提取时转换成list
@TypeConverter
fun getThumbFromString(value: String):List? {
return value.split(",")
}
//存入表字段时list抓好string
@TypeConverter
fun storeThumbToString(list: List): String {
val str = StringBuilder(list[0])
list.forEach {
str.append(",").append(it)
}
return str.toString()
}
}
哪些情况下可以和@TypeConverters一起使用,实现类型转换:
可以理解为**@TypeConverters可以在任何场合下使用,当然前提条件是必须支持修饰那种类型**。
@TypeConverters#value中的对象我们称之为typeConverters对象。
@TypeConverter修饰的方法我们称之为typeConverter方法。
规则如下:
typeConverters对象必须是一个类;
typeConverters对象中必须存在被@TypeConverter修饰的方法,并且是成对的,一个表示转入,一个表示转出;
typeConverters对象是内部类,除非使用@ProvidedTypeConverter修饰,否则必须使用static修饰;
typeConverters对象除非使用@ProvidedTypeConverter修饰,否则支持一下条件中的至少一个条件:
object
或companion object
kotlin类型;typeConverter方法必须public修饰;
typeConverter方法返回类型不允许void(error ,none)类型;
typeConverter方法返回类型如果是泛型,必须是实体类型(如List),不允许出现List< T>或List类型;
typeConverter方法参数必须有且仅有一个;
typeConverter方法参数类型如果是泛型,必须是实体类型(如List),不允许出现List< T>或List类型;
@TypeConverters#value中的typeConverters对象可以有多个,这些对象的所有typeConverter方法不允许出现方法返回类型和方法参数类型都一致的情况;
typeConverters对象中只有@TypeConverter修饰的方法有效,其他方法无任何意义,也不会报错。
还有一种情况(本不想说的,估计用的不大,或者不能用,可以验证一下),已上面的案例为例:@TypeConverters(ThumbConverter::class)在database节点上修饰,在entity节点上没有使用,好像大概可能也是可以的。
感觉篇幅太大,所以额外在写文章继续dao操作数据库。