引言
- 在Android应用开发过程中,几乎每个应用都需要使用到数据库,而直接使用系统的
Sqlite
存储数据的话又不是加密的,并且当我们需要插入多种类型的Entity
时,就需要编写多少种增删改查的SQL
语句,然后拼接或者反射转换,繁琐程度只有写过的老哥才知道。因此,Ummm..为了不写到吐血且保证一定的数据安全性,咱还是选择一个能加密数据库并且能简洁使用的ORM吧。 - 为啥用
GreenDao
呢?宣传描述可查看官网介绍 GreenDao , 简单概述一下就是: 比Sqlite
原生更快、可加密、使用更简单、极小的内存消耗、库体积小于100k等;EventBus
也是出自他们的手笔,可信赖程度高。 - OK,接下来我们会从基本使用着手逐步分析它的内部实现,看看它内部有啥猫腻。
准备工作
- 环境配置:
- 在
projects#build.gradle
文件中如下配置。
- 在
buildscript {
repositories {
jcenter()
mavenCentral() // add repository
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.1'
classpath 'org.greenrobot:greendao-gradle-plugin:3.2.2'
}
}
- 在
app#build.gradle
文件中如下配置 。
apply plugin: 'com.android.application'
apply plugin: 'org.greenrobot.greendao' // apply plugin
dependencies {
implementation 'org.greenrobot:greendao:3.2.2'
// 需要加密数据库的话添加这个
implementation 'net.zetetic:android-database-sqlcipher:3.5.6'
}
接下来需要 建立模型入口(
Model Entity
),只有在创建了至少一个模型入口并且编译通过的情况下才能开始使用greenDao
。-
生成
Entity
相关代码- 生成模板代码的方式有两种:
- 方式一: 编写JAVA代码,然后执行生成,如下示例:
public class MyDaoGenerator { public static void main(String[] args) { //指定生成类所在的包名 Schema schema = new Schema(1, "com.example.laixiaolong.greendaotraning.greenDao.db"); addNote(schema); // addCustomerOrder(schema); // 指定生成的类存放的目录 new DaoGenerator().generateAll(schema, "./app/src/main/java"); } private static void addNote(Schema schema) { Entity note = schema.addEntity("Note"); // 指定类名 // id note.addIdProperty(); // 属性即表的Column字段 note.addStringProperty("title").notNull(); note.addStringProperty("content"); note.addDateProperty("date"); } private static void addCustomerOrder(Schema schema) { Entity customer = schema.addEntity("Customer"); customer.addIdProperty(); customer.addStringProperty("name").notNull(); Entity order = schema.addEntity("Order"); order.setTableName("ORDERS"); // "ORDER" is a reserved keyword order.addIdProperty(); Property orderDate = order.addDateProperty("date").getProperty(); Property customerId = order.addLongProperty("customerId").notNull().getProperty(); order.addToOne(customer, customerId); // 一对一关联 ToMany customerToOrders = customer.addToMany(order, customerId); // 一对多关联 customerToOrders.setName("orders"); customerToOrders.orderAsc(orderDate); } }
- 方式二: 使用
gradle
插件配置:在app#build.gradle
中如下配置,这种方式会便捷很多,因为我们只需要专注于Entity
的编写就好了,编译器会自动生成对应Dao文件。
android { ... } greendao { // 标记当前数据库表版本,*OpenHelpers会根据这个参数来进行版本迁移 // 如果entity或数据库表版本改变一次,这个值就会增加1,默认值是1 schemaVersion 1 // 指定生成的DAO、DaoMaster、DaoSession类所在的包名,默认是Entity所在包下 daoPackage "com.example.laixiaolong.greendaotraning.greenDao.db" // 生成类存放的路径,默认是“build/generated/source/greendao” targetGenDir "./app/src/main/java" // 是否自动生成单元测试 generateTests false // 单元测试存放的路径,默认是“src/androidTest/java” targetGenDirTests "src/androidTest/java" }
- 生成模板代码的方式有两种:
下面一个简单的
ChatObject
关联User
的示例:
@Entity
public class User
{
@Id(autoincrement = true)
private Long id;
@Unique
private Long userIncrId;
@NotNull
private String userAccount;
private String userName;
private Long orgId;
private String orgName;
// ...
}
@Entity
public class ChatObject
{
@Id(autoincrement = true)
private Long id;
private String userIcrId;
// 一对一关联
@ToOne(joinProperty = "userIncrId")
private User user;
}
- 其中
@Entity
属性用法解释如下:@Entity( // 存在多张图时,设置schema参数告诉greenDao这个Entity属于哪张图 // 可以是任意名字 schema = "myschema", // 标识这个entity是否“active”,active状态的表才具备增删改查的方法 active = true, // 指定表的名称,默认是entity的名称 nameInDb = "AWESOME_USERS", // 申明某个列字段会会占用多个列 indexes = { @Index(value = "name DESC", unique = true) }, // 标记是否生成数据表,默认是true,如果false,那么多个entity会映射到一张表,或者在greenDao外创建表 createInDb = false, // 是否需要生成一个具有所有属性的构造函数 // 无参构造器是必须存在的 generateConstructors = true, // 是否需要在属性的get和set方法缺失时自动为其生成 generateGettersSetters = true )
增删改查(基本操作)
-
初始化
- 先在
Application
中如下初始化
public class App extends Application { private DaoSession daoSession; private static App app; @Override public void onCreate() { super.onCreate(); app = this; //使用普通数据库 DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(this, "roz-hjjm-db"); daoSession = new DaoMaster(devOpenHelper.getWritableDb()).newSession(); //使用 加密数据库 // Database encryptedReadableDb = devOpenHelper.getEncryptedReadableDb("123456"); // daoSession = new DaoMaster(encryptedReadableDb).newSession(); } public static App app() { return app; } public DaoSession getDaoSession() { return daoSession; } }
- 先在
-
增删改查
- 使用
GreenDao
时几乎可以完全摆脱手写Sql
的束缚,不必担心某个字段名称写错、关键字写错、标点符号写错的问题,从而更快稳健的实现我们的想法。 - 插入
- 插入方式有以下两种,包括普通插入和使用事务批量插入,两者的区别通过下面代码运行结果可以很清晰的看出(++使用模拟器运行++),同样是插入
100
条数据,如果使用事务批量插入,效率能高个3~4
倍,而如果只需要插入一条,那么对于本例而言(不排除其他情况),两者耗时是一样的,可见:- 当数据量大时还是集中使用批量插入更为妥当
- 如果只需要插入一条,那么二者可任选其一
- 插入方式有以下两种,包括普通插入和使用事务批量插入,两者的区别通过下面代码运行结果可以很清晰的看出(++使用模拟器运行++),同样是插入
@Test public void testGreenDaoInsert() { DaoSession daoSession = App.getDaoSession(); UserDao userDao = daoSession.getUserDao(); long start = System.currentTimeMillis(); // 1. 普通插入,每次只能插入单条 //insert 100: System.out: -->>Insert total used:: 83ms //insert 1:System.out: -->>Insert total used:: 3ms for (int i = 0; i < 100; i++) { // insertOrReplace userDao.insertOrReplace(new User() .setFirstName("firstName::" + i) .setLastName("lastName::" + i) .setGender(i % 2 == 0 ? "male" : "female") .setPhoneNum(String.valueOf(SystemClock.elapsedRealtimeNanos())) .setAge(i + 10)); } // 2. 使用事务插入,可以进行批量插入,也可以单条插入 //insert 100: System.out: -->>Insert total used:: 26ms //insert 1:System.out: -->>Insert total used:: 3ms /*User[] usersArray = new User[100]; for (int i = 0; i < 100; i++) { // insertOrReplace usersArray[i] = new User() .setFirstName("firstName::" + i) .setLastName("lastName::" + i) .setGender(i % 2 == 0 ? "male" : "female") .setPhoneNum(String.valueOf(SystemClock.elapsedRealtimeNanos())) .setAge(i + 10) ; } userDao.insertOrReplaceInTx(usersArray);*/ System.out.println(String.format("-->>Insert total used:: %sms" , (System.currentTimeMillis() - start)) ); }
- 修改
- 修改过程也很简单,并且可以单条修改和使用事务批量修改,和插入方式是一致的,不过需要注意的是:
- 修改时需要设置要修改条目的id(也就是key),否则会修改失败
- 修改过程是覆盖式的,意味着如果修改时
Entity
中某个字段值为null
,那么就会覆盖掉表中对应的字段数据,这点尤为重要,因为可能小手一抖,你的数据就没了
- 修改过程也很简单,并且可以单条修改和使用事务批量修改,和插入方式是一致的,不过需要注意的是:
// 修改 @Test public void testGreenDaoUpdate() { DaoSession daoSession = App.getDaoSession(); UserDao userDao = daoSession.getUserDao(); User user = new User() .setId(51L) .setFirstName("horseLai") .setAge(23) .setPhoneNum("1234"); // before: User{id=51, phoneNum='5866201745180', firstName='firstName::7', gender='female', lastName='lastName::7', age=17} // after: User{id=51, phoneNum='1234', firstName='horseLai', gender='null', lastName='null', age=23} // 1. 普通修改 ,只能单条修改 userDao.update(user); // 2. 批量修改, 可批量修改 // userDao.updateInTx(user); }
- 删除
- 删除过程也很简单,有以下四种方式进行,具体可按需选择,如果知道
id
那么1
和3
都ok
,如果需要指定删除条件,那么4
,删除所有数据则2
。
- 删除过程也很简单,有以下四种方式进行,具体可按需选择,如果知道
// 删除 @Test public void testGreenDaoDelete() { DaoSession daoSession = App.getDaoSession(); UserDao userDao = daoSession.getUserDao(); // 1. 删除单个 // userDao.deleteByKey(15L); // 2. 删除所有 userDao.deleteAll(); // 3. 批量删除(前提是知道key,也就是id) userDao.deleteByKeyInTx(15L, 16L,17L); // 4. 批量(按查找条件)删除 userDao.queryBuilder() .where(UserDao.Properties.Age.gt(15)) .buildDelete() .executeDeleteWithoutDetachingEntities(); }
- 查找
- 查找也很容易,可以按需选择,在以下实例中已经体现的很清楚,相对来说如果条件简单的话,使用
sql
语句进行查询(方式2
)会简洁很多,其次是使用方式1
,最后是使用方式3
,不过我感觉没人会喜欢使用方式3
,除非有什么特殊需求,毕竟看到那一大串代码你就想敬而远之,还有可能会忘记释放cursor
,导致内存泄漏。
- 查找也很容易,可以按需选择,在以下实例中已经体现的很清楚,相对来说如果条件简单的话,使用
// 查找 @Test public void testGreenDaoQuery() { DaoSession daoSession = App.getDaoSession(); UserDao userDao = daoSession.getUserDao(); // 1. 查询 /*List
users = userDao.queryBuilder() .limit(10) .orderAsc(UserDao.Properties.Age) .where(UserDao.Properties.Age.gt(15)) .build() .list();*/ // 2. 通过sql语句查询 /*List users = userDao.queryRaw("where age = ?", "15"); for (User user : users) { System.out.println(user.toString()); }*/ // 3. 通过cursor查询 Cursor cursor = userDao.queryBuilder() .limit(10) .orderAsc(UserDao.Properties.Age) .where(UserDao.Properties.Age.gt(15)) .buildCursor().query(); try { if (cursor != null) { System.out.println("-->> cursor count::" + cursor.getCount()); if (cursor.getCount() == 0) { return; } String[] columnNames = cursor.getColumnNames(); System.out.println("-->> columnNames::" + Arrays.toString(columnNames)); StringBuilder sb = new StringBuilder(); cursor.moveToFirst(); while (cursor.moveToNext()) { String firstName = cursor.getString(cursor.getColumnIndex(UserDao.Properties.FirstName.columnName)); String lastName = cursor.getString(cursor.getColumnIndex(UserDao.Properties.LastName.columnName)); String gender = cursor.getString(cursor.getColumnIndex(UserDao.Properties.Gender.columnName)); String phone = cursor.getString(cursor.getColumnIndex(UserDao.Properties.PhoneNum.columnName)); int age = cursor.getInt(cursor.getColumnIndex(UserDao.Properties.Age.columnName)); sb.append("\n{\nfirstName=").append(firstName).append(",\n") .append("lastName=").append(lastName).append(", \n") .append("gender=").append(gender).append(", \n") .append("phone=").append(phone).append(", \n") .append("age=").append(age).append(", \n") .append("}"); } System.out.println(sb.toString()); } } finally { if (cursor != null) cursor.close(); } // 统计相应条件下的条目数量,如果需要查询数量,而不需要提取数据, // 那么请使用这种方式,不然性能浪费是明显的。 /* long totalCount = userDao.queryBuilder() .limit(10) .orderAsc(UserDao.Properties.Age) .where(UserDao.Properties.Age.gt(15)) .buildCount() .count(); System.out.println("-->>Total Count::" + totalCount);*/ } - 使用
关系型数据库
关系型数据库几乎覆盖了我们大部分实际使用场景,比如说实时通讯场景、商城等等。这里我们以一个博客系统数据库模型为例,熟悉一下如何使用greenDao
建立这些场景模型。
-
复合场景
- 假设我们存在一篇博客,那么肯定需要关联若干条评论数量,并且需要知道是谁写的吧,那么需要关联一个用户(发布者);因此对于评论而言,我们也需要知道是是谁评论的吧,那么我们也需要对每条评论关联一个用户; 如果我们还需要用户可以评论用户评论,那么我们还需要在用户评论中关联若干个用户评论的回复评论;这样的话,这个模型中存在两种关联关系:
- 一对一关联:比如这里的博客与发布者的关系,一篇博客对应于一个发布者。
- 一对多关联:比如这里的博客与评论的关系,一篇博客对应与若干条评论。
- 假设我们存在一篇博客,那么肯定需要关联若干条评论数量,并且需要知道是谁写的吧,那么需要关联一个用户(发布者);因此对于评论而言,我们也需要知道是是谁评论的吧,那么我们也需要对每条评论关联一个用户; 如果我们还需要用户可以评论用户评论,那么我们还需要在用户评论中关联若干个用户评论的回复评论;这样的话,这个模型中存在两种关联关系:
-
根据以上这个相对复杂的应用场景,可以如下建立数据库模型:
- 博客
@Entity public class Blog { @Id(autoincrement = true) @Unique private long id; @NotNull private String title; private String content; @NotNull private Date createTime; // 发布者 // 关联至User表的PRIMARY KEY,也就是id @NotNull private long userId; @ToOne(joinProperty = "userId") private User user; // 评论 // JOIN Comment表中的commentId列至本表的commentId列 @NotNull private long commentId; // 1. 简单JOIN // @ToMany(referencedJoinProperty = "commentId") // 2. 可以存在多个复杂的JOIN,或者你希望指定JOIN进来的列的列名 @ToMany(joinProperties = {@JoinProperty(name = "commentId", referencedName = "id")}) @OrderBy("createTime ASC") List
comments; } - 评论
- 用于评论博客
@Entity public class Comment { @Id(autoincrement = true ) private long id; @NotNull private String title; private String content; @NotNull private Date createTime; // 评论发布者 // 关联至User表的PRIMARY KEY,也就是id private long userId; @ToOne(joinProperty = "userId") private User user; // 评论 // JOIN 关联CommentReply中的commentSubId列 private long commentSubId; @ToMany( joinProperties = {@JoinProperty( name = "commentSubId" ,referencedName = "id")}) @OrderBy("createTime ASC") List
subComments; } - 评论的回复评论
- 某个用户对博客发表了一条评论,其他用户对这条评论的评论
@Entity public class CommentReply { @Id(autoincrement = true ) private long id; @NotNull @Unique private long commentSubId; @NotNull private String title; private String content; @NotNull private Date createTime; // 评论发布者 // 关联至User表的PRIMARY KEY,也就是id private long userId; @ToOne(joinProperty = "userId") private User user; }
-
那么如何操作这个关联数据库模型呢? 可以参考如下示例代码。
- 注意,我的代码只起到演示作用,主要用于阐述如何使用
GreenDao
建立数据关联,实际应用请思考再三,否则出问题了可别怪我哈。
- 注意,我的代码只起到演示作用,主要用于阐述如何使用
@Test
public void multiRelative() {
UserDao userDao = App.getDaoSession().getUserDao();
BlogDao blogDao = App.getDaoSession().getBlogDao();
CommentDao commentDao = App.getDaoSession().getCommentDao();
CommentReplyDao commentReplyDao = App.getDaoSession().getCommentReplyDao();
// 1. 发布一篇博客
// 发布者
User publisher = new User();
publisher.setAge(24)
.setGender("男")
.setFirstName("horseLai")
.setLastName("horseLai")
.setPhoneNum("22132323323");
userDao.insertOrReplace(publisher);
// 博客
Blog blog = new Blog();
blog.setTitle("GreenDao建立关系型数据库模型");
blog.setContent("xxxxxxxxx");
blog.setCreateTime(Calendar.getInstance().getTime());
blog.setUser(publisher); // 关联至发布者
blogDao.insertOrReplace(blog);
// 2. 添加一条评论
// 评论者
User commentUser = new User()
.setPhoneNum("1234555")
.setAge(24)
.setFirstName("horseLai");
userDao.insertOrReplace(commentUser);
// 评论
Comment comment = new Comment();
comment.setTitle("评论");
comment.setContent("评论内容");
comment.setUser(commentUser); // 关联评论者
comment.setCreateTime(Calendar.getInstance().getTime());
commentDao.insertOrReplace(comment);
// 3. 新增一条评论的回复评论
User commentReplyUser = new User()
.setPhoneNum("12345568875")
.setAge(23)
.setFirstName("horseLai");
userDao.insertOrReplace(commentReplyUser);
// 回复的评论
CommentReply commentReply = new CommentReply();
commentReply.setTitle("回复评论");
commentReply.setContent("评论回复内容");
commentReply.setUser(commentReplyUser);
commentReply.setCreateTime(Calendar.getInstance().getTime());
commentReplyDao.insertOrReplace(commentReply);
}
- 查找数据
- 增删该查和之前讲解的内容是一致的,这里只演示简单查找,如下示例代码:
@Test public void query() { BlogDao blogDao = App.getDaoSession().getBlogDao(); List
blogs = blogDao.queryBuilder() .build() .list(); blogs.forEach((blog -> { System.out.println(); System.out.println(blog.toString()); System.out.println(); })); } - 数据结果如下:
- 可见其实整个模型是个类似于树形的关系结构。
Blog{ id=0, title='GreenDao建立关系型数据库模型', content='xxxxxxxxx', createTime=Tue Oct 02 14:03:27 GMT 2018, userId=1, user=User{id=1, phoneNum='22132323323', firstName='horseLai', gender='男', lastName='horseLai', age=24}, commentId=0, comments=[ Comment{ id=0, title='评论', content='评论内容', createTime=Tue Oct 02 14:03:27 GMT 2018, userId=2, user=User{ id=2, phoneNum='1234555', firstName='horseLai', gender='null', lastName='null', age=24 }, subComments=[ CommentReply{ id=0, commentSubId=0, title='回复评论', content='评论回复内容', createTime=Tue Oct 02 14:03:27 GMT 2018, userId=3, user=User{ id=3, phoneNum='12345568875', firstName='horseLai', gender='null', lastName='null', age=23 } } ] } ] }