其他
前人用GreenDao留下的坑,全线被扣了绩效
/ 今日科技快讯 /
近日在2022年校园和社会招聘计划中,OPPO计划再招聘技术研发岗位超过2000人,核心招聘领域涉及硬件研发、软件研发、底层技术研发(含芯片研发)、多媒体软件研发、计算机视觉、语音语义、数字孪生、大数据、云计算、AI工程化等。
/ 作者简介 /
今天周五,明天继续上班,大家加油!
本篇文章来自leobert-lan的投稿,分享了GreenDao踩坑后的深度思考,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。
leobert-lan的博客地址:
https://juejin.cn/user/2066737589654327/posts
/ 前言 /
本篇文章,您将从一个GreenDao使用的事故开始,围观事故现场,并获得问题分析结论。跟随作者再次巩固GreenDao的整体设计,并实践 APT 、 Gradle Plugin 两种方案,通过不断地总结、对比和深度反思扫荡盲区,将知识融会贯通!
背景是这样的:
项目的部分业务数据存储于 本地数据库 数据库业务使用了ORM框架--GreenDao 采用了类似 GreenDaoUpgradeHelper(https://github.com/yuweiguocn/GreenDaoUpgradeHelper) 的方案处理 "数据库版本升级"
前人采用的数据库升级方案就很危险 特殊渠道包的更新频次低、时间跨度长,测试覆盖粒度不够细(仅回归主功能、增量实现、从主包同步的bug修改和优化)导致一直未发现问题 轻易地相信了一个老项目,没有对基建部分进行详细的review
性能问题 临时表名产生的制约 人工维护传入的dao -- 直接导致的事故
需要人工维护升级的dao参数 -- 人的记性差,容易遗漏。不符合GreenDaoUpgradeHelper 等工具的设计初衷,即不需要人工维护升级细节 restore时的效率问题 临时表名无形中产生的制约
GreenDao 如何进入升级(降级)
PRAGMA user_version;
#设置
PRAGMA user_version = {version}
schemaVersion 1000
}
public static final int SCHEMA_VERSION = 1000;
//...
public static abstract class OpenHelper extends DatabaseOpenHelper {
public OpenHelper(Context context, String name) {
super(context, name, SCHEMA_VERSION);
}
public OpenHelper(Context context, String name, CursorFactory factory) {
super(context, name, factory, SCHEMA_VERSION);
}
//...
}
}
try {
if (version == 0) {
onCreate(db);
} else {
if (version > mNewVersion) {
onDowngrade(db, version, mNewVersion);
} else {
onUpgrade(db, version, mNewVersion);
}
}
db.setVersion(mNewVersion);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
升级Helper概览
删除临时表 DROP TABLE IF EXISTS {tempTableName} -- 看似没有毛病,但如果存在业务设计的临库临表,就被误删除了 创建临时表 CREATE TEMPORARY TABLE {tempTableName} AS SELECT * FROM {oTableName}
先判断临时表名是否存在,如存在则抛错 然后再判断新增表是否会和临时表重名,如果存在则抛错 继而在同一数据库内使用 ALTER TABLE {oTableName} RENAME TO {tempTableName} 修改表名
此处仅为一个设想,是Sqlite支持的SQL,但并未在Android项目中实践验证以及推理可能出现的问题。 可以预见的是:即便增加校验,也无法避免用户绕开GreenDao进行数据库操作所带来的隐性冲突可能。 GreenDaoUpgradeHelper在新的临时数据库中处理临时表,作者公司项目中的代码在原数据库中处理
小结
方案1:注解处理技术
@Index(value = "text, date DESC", unique = true)
})
public class Note {
@Id
private Long id;
@NotNull
private String text;
private String comment;
private java.util.Date date;
@Convert(converter = NoteTypeConverter.class, columnType = String.class)
private NoteType type;
}
方案2:GreenDao插件
Entity 发现 Entity 中表字段关系、索引、约束分析,源码级代码插桩 Dao 生成 DaoMaster生成
* Master of DAO (schema version 1000): knows all DAOs.
*/
public class DaoMaster extends AbstractDaoMaster {
//...
}
* Annotation for entities
* greenDAO only persist objects of classes which are marked with this annotation
*/
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface Entity {
/**
* Specifies the name on the DB side (e.g. table name) this entity maps to. By default, the name is based on the entities class name.
*/
String nameInDb() default "";
/**
* Indexes for the entity.
* <p/>
* Note: To create a single-column index consider using {@link Index} on the property itself
*/
Index[] indexes() default {};
/**
* Advanced flag to disable table creation in the database (when set to false). This can be used to create partial
* entities, which may use only a sub set of properties. Be aware however that greenDAO does not sync multiple
* entities, e.g. in caches.
*/
boolean createInDb() default true;
/**
* Specifies schema name for the entity: greenDAO can generate independent sets of classes for each schema.
* Entities which belong to different schemas should <strong>not</strong> have relations.
*/
String schema() default "default";
/**
* Whether update/delete/refresh methods should be generated.
* If entity has defined {@link ToMany} or {@link ToOne} relations, then it is active independently from this value
*/
boolean active() default false;
/**
* Whether an all properties constructor should be generated. A no-args constructor is always required.
*/
boolean generateConstructors() default true;
/**
* Whether getters and setters for properties should be generated if missing.
*/
boolean generateGettersSetters() default true;
/**
* Define a protobuf class of this entity to create an additional, special DAO for.
*/
Class protobuf() default void.class;
}
实现 AbstractProcessor 并完成SPI注册,进入到AnnotationProcessor流程 通过获取 Entity 注解类对应的 TypeElement ,判断项目是否正确配置 获取被注解的类,收集必要的信息 可选项1:甄别是否会出现临时表表名冲突,编译期抛错优于运行期 可选项2:排除Kotlin类等干扰项,GreenDao仅支持Java 生成代码
额外的风险
GreenDaoCollector 没有考虑注解中 boolean createInDb() default true; 等方法 GreenDao存在一些限制,例如不支持Kotlin,GreenDaoCollector 采用了一个取巧的方案来甄别排除不支持的类 越精密的机器越容易出现故障,对于复杂的机制也是如此
莫非命也,顺受其正,是故知命者不立乎岩墙之下。尽其道而死者,正命也;桎梏死者,非正命也。防祸于先而不致于后伤情。知而慎行,君子不立于危墙之下,焉可等闲视之-- 《孟子》
greendaoPrepare greendao
窥一斑而见全豹--分析其设计
1649475279008
{略去}/GreenDaoCollector/app/src/main/java/osp/leobert/android/gdc/entity/JavaDemoEntity.java
{略去}/GreenDaoCollector/app/src/main/java/osp/leobert/android/gdc/entity/JavaDemoEntityTemp.java
greendao-api(https://github.com/greenrobot/greenDAO)开源,greendao中的注解和基础interface greendao-generator (https://github.com/greenrobot/greenDAO)开源,生成源码部分 greenrobot-jdt,Repackaged version of JDT essentials (https://github.com/greenrobot/essentials), 开源,用于计算Hash
通过 greendaoPrepare 任务,基于源码内容做字符串检索,快速筛选出可能是Entity的源码,信息输出到文件:greendao-candidates.list 读取greendao-candidates.list 文件内容,基于jdt分析其源码语法树(AST) 基于AST和注解解析Entity、主键、索引、约束、关联等 调用greendao-generator生成源码
大象无形--利用加载机制做文章
private Configuration getConfiguration(String probingTemplate) throws IOException {
Configuration config = new Configuration(Configuration.VERSION_2_3_29);
config.setClassForTemplateLoading(getClass(), "/");
}
}
setTemplateLoader(new ClassTemplateLoader(resourceLoaderClass, basePackagePath));
}
/**
* A {@link TemplateLoader} that can load templates from the "classpath". Naturally, it can load from jar files, or from
* anywhere where Java can load classes from. Internally, it uses {@link Class#getResource(String)} or
* {@link ClassLoader#getResource(String)} to load templates.
*/
public class ClassTemplateLoader extends URLTemplateLoader {
//ignore 看类注释
}
关键点在于 ClassLoader#getResource 并且存在 Parent-delegate 机制。 只需要在plugin中增加同名模板,在plugin被运行时,该模板将被先加载。
新建插件,通过继承或者使用组合,直接使用GreenDao插件逻辑,选用组合,因为无法继承 新增同名模板,并增加相关逻辑用以生成代码,最终选用dao-master模板
class GreenDaoPluginWrapper : Plugin<Project> {
private val wrapper: Plugin<Project> = Greendao3GradlePlugin()
override fun apply(project: Project) {
wrapper.apply(project)
}
}
/**
* Master of DAO (schema version ${schema.version?c}): knows all DAOs.
*/
public class ${schema.prefix}DaoMaster extends AbstractDaoMaster{
public static final int SCHEMA_VERSION=${schema.version?c};
// all dao need to create in db, do not modify, created by leobert
public static final List<Class<? extends AbstractDao<?, ?>>>allDao=new java.util.ArrayList();
static {
<#list schema.entities as entity>
<#if!entity.skipCreationInDb>
allDao.add(${entity.classNameDao}.class);
</#if>
</#list>
}
//其他略
}
/**
* Master of DAO (schema version 1): knows all DAOs.
*/
public class DaoMaster extends AbstractDaoMaster {
public static final int SCHEMA_VERSION = 1;
// all dao need to create in db, do not modify, created by leobert
public static final List<Class<? extends AbstractDao<?, ?>>> allDao = new java.util.ArrayList();
static {
allDao.add(JavaDemoEntityDao.class);
}
/** Creates underlying database table using DAOs. */
public static void createAllTables(Database db, boolean ifNotExists) {
JavaDemoEntityDao.createTable(db, ifNotExists);
}
/** Drops underlying database table using DAOs. */
public static void dropAllTables(Database db, boolean ifExists) {
JavaDemoEntityDao.dropTable(db, ifExists);
}
//其他略
}
与方案1对比
前车已覆,后未知更,何觉时?-- 《荀子·成相》
前人采用的数据库升级方案就很危险 特殊渠道包的更新频次低、时间跨度长,测试覆盖粒度不够细,导致一直未发现问题 轻易地相信了一个老项目,没有对基建部分进行详细的review
需要人工维护升级的dao参数,容易遗漏,存在风险 restore时的效率问题 临时表名无形中产生的制约
轻易地相信了一个老项目,没有对基建部分进行详细的review Master of DAO (schema version 1000): knows all DAOs.
SQLiteDatabase.create(null)
).newSession().allDaos.map {
it.javaClass
}.toCollection(arrayListOf())
pom文件摘要
<groupId>org.greenrobot</groupId>
<artifactId>greendao-gradle-plugin</artifactId>
<version>3.3.0</version>
<name>greenDAO Gradle Plugin</name>
<description>Gradle Plugin for greenDAO, the light and fast ORM for Android</description>
<url>https://github.com/greenrobot/greenDAO</url>
<!-- 略-->
<dependencies>
<dependency>
<groupId>org.greenrobot</groupId>
<artifactId>greendao-code-modifier</artifactId>
<version>3.3.0</version>
<scope>compile</scope>
</dependency>
<!-- 略-->
</dependencies>
<!--greendao-code-modifier-3.3.0.pom-->
<groupId>org.greenrobot</groupId>
<artifactId>greendao-code-modifier</artifactId>
<version>3.3.0</version>
<name>greenDAO Code Modifier</name>
<description>Code modifier for greenDAO, the light and fast ORM for Android</description>
<url>https://github.com/greenrobot/greenDAO</url>
<!-- 略-->
<dependencies>
<dependency>
<groupId>org.greenrobot</groupId>
<artifactId>greendao-api</artifactId>
<version>3.3.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.greenrobot</groupId>
<artifactId>greendao-generator</artifactId>
<version>3.3.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.greenrobot</groupId>
<artifactId>greenrobot-jdt</artifactId>
<version>3.20.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.greenrobot</groupId>
<artifactId>essentials</artifactId>
<version>3.0.0-RC1</version>
<scope>compile</scope>
</dependency>
<!-- 略-->
</dependencies>