简单介绍
greenDAO是一个开源的Android ORM,使SQLite数据库的开发再次变得有趣。 它减轻了开发者处理底层的数据库需求,同一时候节省开发时间。
SQLite是一个非常不错的关系型数据库。 尽管如此,编写SQL和解析查询结果仍然是相当乏味和耗时的任务。 greenDAO通过将Java对象映射到数据库表(称为ORM,“对象/关系映射”)来解决这些问题。
这样。您能够使用简单的面向对象的API来存储,更新。删除和查询Java对象。
特性
- 最高性能(可能是Android中最快的ORM); 我们的benchmarks 也是开源的
- 涵盖关系和连接且易用的强大API
- 最小的内存开销
- 迷你开源库(<100KB),减少工程构建时间同一时候避免65k方法限制
- 数据库加密:greenDAO支持SQLCipher以保证用户的数据安全
- 强大的社区:超过5000 GitHub星星表明我们拥有一个强大而活跃的社区
開始使用
入门
本教程将引导你完毕一个简单的greenDAO演示样例项目DaoExample。 克隆代码并运行它,或者直接在GitHub上查看文件。
DaoExample是一个简单地用来记笔记的Android应用程序。
你能够通过键入一些文本来加入新笔记,并通过点击现有笔记删除笔记。
1. Note实体和DAO类
一起看下代码:在src文件夹中,你会发现一个实体类Note.java。 它被持久化到数据库并包括Note的全部数据。如id,凝视文本和创建日期。
@Entity(indexes = {
@Index(value = "text, date DESC", unique = true)
})
public class Note {
@Id
private Long id;
@NotNull
private String text;
private Date date;
...
通常,实体是在数据库中持久化的类(比如。一个对象的一行)。 实体包括映射到数据库列的属性。
如今開始编译工程。比如在Android Studio中使用“Build> Make project”。 这将触发greenDAO来生成DAO类。比如NoteDao.java。这将帮助我们向数据库加入笔记。
2. 插入和删除笔记
要学习怎样加入一些笔记。请看一下NoteActivity类。 首先。我们必须为我们的Note类准备一个DAO对象,我们在onCreate()中做:
// get the note DAO
DaoSession daoSession = ((App) getApplication()).getDaoSession();
noteDao = daoSession.getNoteDao();
当用户单击加入button时。将调用addNote()方法。 在这里,我们创建一个新的Note对象。并将其传递给DAO insert()方法,以将其插入到数据库中:
Note note = new Note(null, noteText, comment, new Date());
noteDao.insert(note);
Log.d("DaoExample", "Inserted new note, ID: " + note.getId());
注意。我们在创建笔记的时候没有传递id。 在这样的情况下,数据库决定note id。 DAO负责在从插入操作返回之前自己主动设置新的ID(请參阅日志语句)。
删除笔记也非常easy。參见NoteClickListener:
noteDao.deleteByKey(id);
你能够探索NoteDao的其它方法,比方loadAll()和update()。
3. 设置数据库
你已经看到了DAO。可是怎样初始化greenDAO和底层数据库呢? 通常你须要init一个DaoSession。它通常在整个应用程序中的Application类中运行一次:
DevOpenHelper helper = new DevOpenHelper(this, "notes-db");
Database db = helper.getWritableDb();
daoSession = new DaoMaster(db).newSession();
使用生成的DaoMaster类提供的帮助器类DevOpenHelper创建数据库。 它是DaoMaster中的OpenHelper类的一个实现,它为你创建全部数据库。 再也不须要编写“CREATE TABLE”语句。
然后Activity和Fragment能够调用getDaoSession()来訪问全部实体DAO,和我们之前在插入和删除的时候一样。
4. 扩展和加入实体
为了扩展我们的笔记或创建新实体,你仅仅需改动或创建Java类。并以同样的方式注解它们。
然后又一次编译项目。
有关具体信息。请參阅建模实体。
(这个下面已经翻译出来)
介绍
greenDAO是Android的对象/关系映射(ORM)工具。 它为关系数据库SQLite提供了一个面向对象的接口。像greenDAO一类的ORM工具为你做非常多反复性的任务,提供简单的数据接口。
1. Gradle插件和DAO代码生成
为了在你的Android项目中使用greenDAO。你须要加入greenDAO Gradle插件并加入greenDAO库:
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.greenrobot:greendao-gradle-plugin:3.2.1'
}
}
apply plugin: 'com.android.application'
apply plugin: 'org.greenrobot.greendao'
dependencies {
compile 'org.greenrobot:greendao:3.2.0'
}
然后进行实体建模也就是创建实体,并编译项目,比如在Android Studio中使用“Build> Make Project”。
2. 核心类
一旦项目构建完毕,你就能够在Android项目中開始使用greenDAO了。
下面核心类是greenDAO的基本接口:
DaoMaster:使用greenDAO的切入点。
DaoMaster保存数据库对象(SQLiteDatabase)并管理特定模式的DAO类(而不是对象)。
它有静态方法来创建表或删除它们。 它的内部类OpenHelper和DevOpenHelper都是SQLiteOpenHelper的实现,用来在SQLite数据库中创建模式。
DaoSession:管理特定模式的全部可用DAO对象,你能够使用当中一个的getter方法获取DAO对象。 DaoSession还为实体提供了一些通用的持久性方法,如插入,载入,更新,刷新和删除。 最后。DaoSession对象也跟踪identity scope。
有关很多其它具体信息,请查看会话文档
DAO:数据訪问对象(DAO),用于实体的持久化和查询。
对于每一个实体,greenDAO会生成一个DAO。 它比DaoSession拥有很多其它的持久化方法,比如:count,loadAll和insertInTx。
实体:可持久化对象。
通常,实体是使用标准Java属性(如POJO或JavaBean)表示数据库行的对象。
3. 核心初始化
最后,下面代码演示样例说明了初始化数据库和核心greenDAO类的第一步
//做一次,比如在你的Application类中
helper = new DaoMaster.DevOpenHelper(this。“notes-db”。null);
db = helper.getWritableDatabase();
daoMaster = new DaoMaster(db);
daoSession = daoMaster.newSession();
//在您的活动/片段中运行此操作以获取DAO
noteDao = daoSession.getNoteDao();
该演示样例假设存在一个Note实体。
有了它的DAO(noteDao对象)。我们能够调用这个特定实体的持久化操作。
实体建模
要在项目中使用greenDAO。您须要创建一个表示应用程序中持久数据的实体模型。 然后,基于此模型。greenDAO为DAO类生成Java代码。
模型本身是使用带有注解的Java类定义的。
要使用旧式生成器创建模式,请參阅生成器。
下側的图示描写叙述了greenDAO所基于的元模型。
1. 模式
无需不论什么其它配置就能够開始使用greenDAO Gradle插件。可是。至少应该像下面这样设置下模式版本号:
// In the build.gradle file of your app project:
android {
...
}
greendao {
schemaVersion 2
}
此外,greendao配置元素支持一系列配置选项:
- schemaVersion:数据库模式的当前版本号。 这由* OpenHelpers类用于在模式版本号之间迁移。 假设更改实体/数据库模式,则必须添加此值。
默认值为1。
- daoPackage:生成的DAO,DaoMaster和DaoSession的包名称。 默觉得源实体的包名称。
- targetGenDir:生成的源代码应存储在的位置。 默觉得生成文件夹(build / generated / source / greendao)中生成的源文件夹。
- generateTests:设置为true以自己主动生成单元測试。
- targetGenDirTests:生成的单元測试应存储在的基本文件夹。 默觉得src / androidTest / java。
2. 实体和注解
greenDAO 3使用注解来定义模式和实体。 这里有一个简单的样例:
@Entity
public class User {
@Id
private Long id;
private String name;
@Transient
private int tempUsageCount; // not persisted
// getters and setters for id and user ...
}
@Entity注解将Java类User转换为数据库支持的实体。 这也将指示greenDAO生成必要的代码(比如DAO)。
注意:仅支持Java类。 假设你喜欢还有一种语言,如Kotlin。你的实体类仍然必须是Java。
5. @Entity 注解
正如在上面的演示样例中看到的,@Entity注解将Java类标记为greenDAO的持久化的实体。
尽管通常没有不论什么额外的參数,在使用@Entity的时候你仍然能够配置一些细节:
@Entity(
// If you have more than one schema, you can tell greenDAO
// to which schema an entity belongs (pick any string as a name).
schema = "myschema",
// Flag to make an entity "active": Active entities have update,
// delete, and refresh methods.
active = true,
// Specifies the name of the table in the database.
// By default, the name is based on the entities class name.
nameInDb = "AWESOME_USERS",
// Define indexes spanning multiple columns here.
indexes = {
@Index(value = "name DESC", unique = true)
},
// Flag if the DAO should create the database table (default is true).
// Set this to false, if you have multiple entities mapping to one table,
// or the table creation is done outside of greenDAO.
createInDb = false,
// Whether an all properties constructor should be generated.
// A no-args constructor is always required.
generateConstructors = true,
// Whether getters and setters for properties should be generated if missing.
generateGettersSetters = true
)
public class User {
...
}
注意,当使用Gradle插件时。眼下不支持多个模式。 暂时,继续使用你的生成器项目。
5. 基本属性
@Entity
public class User {
@Id(autoincrement = true)
private Long id;
@Property(nameInDb = "USERNAME")
private String name;
@NotNull
private int repos;
@Transient
private int tempUsageCount;
...
}
@Id注解选择long / Long属性作为实体ID。 在数据库术语中。它是主键。
參数autoincrement是一个标志。使ID值不断添加(不重用旧值)。
@Property同意你定义一个当前属性映射到的数据库列的非默认名称。 假设为空。greenDAO将以SQL-ish方式使用字段名(大写字母。下划线取代驼峰,比如customName将成为CUSTOM_NAME)。 注意:当前仅仅能使用内联常量来指定列名称。
@NotNull使属性在数据库端为“NOT NULL”列。
通常,使用@NotNull标记原始类型(long,int。short,byte),同一时候使用封装类(Long,Integer。Short,Byte)能够使用空值。
@Transient标记要从持久性中排除的属性。
将它们用于暂时状态等。
或者。也能够使用Java中的transient关键字。
6. 主键限制
眼下,实体必须具有long或Long属性作为其主键。
这是Android和SQLite的推荐做法。
要解决此问题。请将你的键属性定义为其它属性,但为其创建唯一索引:
@Id
private Long id;
@Index(unique = true)
private String key;
7. 属性索引
在属性中使用@Index可为对应的数据库列创建数据库索引。 使用下面參数自己定义:
name:假设你不喜欢greenDAO为索引生成的默认名称。你能够在这里指定你的名字。
unique:向索引加入UNIQUE约束。强制全部值是唯一的。
@Entity
public class User {
@Id private Long id;
@Index(unique = true)
private String name;
}
@Unique向数据库列加入UNIQUE约束。
注意。SQLite也隐式地为它创建一个索引。
@Entity
public class User {
@Id private Long id;
@Unique private String name;
}
8. 默认值
greenDAO尝试使用合理的默认值。以便开发者每一个都一一配置。
比如。数据库側的表和列名称派生自实体和属性名称。 而不是在Java中使用的骆驼案例样式。默认数据库名称使用大写,使用下划线分隔单词。
比如。名为creationDate的属性将成为数据库列CREATION_DATE
9. 关系
要了解怎样加入一对一和多对多关系,请參阅关系(下面已经翻译)
10. 触发生成
一旦实体模式就位,你就能够通过在IDE中使用“Make project”来触发代码生成过程。
或者直接运行greendao Gradle任务。
假设在更改实体类之后遇到错误,请尝试又一次生成项目。以确保清除旧生成的类。
11. 改动生成的代码
greenDAO 3中的实体类由开发者创建和编辑。
然而,在代码生成过程中。greenDAO可能会添加实体的源代码。
greenDAO将为它创建的方法和字段加入一个@Generated注解,以通知开发者并防止不论什么代码丢失。
在大多数情况下,你不必关心使用@Generated注解的代码。
作为预防措施,greenDAO不会覆盖现有代码,而且假设手动更改生成的代码会引发错误:
Error:Execution failed for task ':app:greendao'.
> Constructor (see ExampleEntity:21) has been changed after generation.
Please either mark it with @Keep annotation instead of @Generated to keep it untouched,
or use @Generated (without hash) to allow to replace it.
正如错误消息所暗示的。通常有两种方法来解决此问题:
- 将更改还原为使用@Generated注解的代码。
或者。你也能够全然删除更改的构造函数或方法。 它们将与下一个版本号又一次生成。
- 使用@Keep注解替换@Generated注解。
这将告诉greenDAO永远不要触摸带注解的代码。 请记住,你的更改可能会中断实体和其它greenDAO之间的约定。 此外。未来的greenDAO版本号可能期望生成的方法中有不同的代码。 所以,要慎重。。
同一时候採用单元測试的方法来避免麻烦是个不错的选择。
12. 保留部分
不再支持旧版本号的greenDAO中使用的KEEP。
可是,假设Gradle插件检測到KEEP FIELDS部分。它将自己主动使用@Transient凝视字段。 之后。可能会删除周围的KEEP FIELDS凝视。
会话和会话的identity scope
(生成的)DaoSession类是greenDAO的中心接口之中的一个。 DaoSession提供开发者訪问基本实体操作和相较于DAO的更完整的操作集合。
此外,会话还管理实体的identity scope。
1. DaoMaster and DaoSession
如“入门”部分所述。你须要创建一个DaoMaster以获取DaoSession:
daoMaster = new DaoMaster(db);
daoSession = daoMaster.newSession();
请注意,数据库连接属于DaoMaster部分。因此多个会话指的是同一个数据库连接。
因此,我们能够相当快地创建新会话。
可是。每一个会话得分配存储空间,特别是实体的会话“缓存”。
2. Identity scope 和会话缓存
假设有两个查询返回同样的数据库对象,则使用多少个Java对象:一个或两个? 它全然取决于Identity scope。
greenDAO中的默认值(行为是可配置的)是多个查询返回对同一Java对象的引用。 比如。从ID为42的USER表中载入User对象会为这两个查询返回同样的Java对象。
这样的方式的副作用是某种实体“缓存”。 假设实体对象在内存中仍然存在(greenDAO在这里使用弱引用),则不会再次构造该实体。 此外,greenDAO不运行数据库查询以更新实体值。 相反。对象从会话快速缓存“马上”返回,这个速度是相当快的。
3. 清除identity scope
要清除整个会话的identity scope,以便不返回“缓存”对象:
daoSession.clear();
清除单个DAO的identity scope:
noteDao = daoSession.getNoteDao();
noteDao.detachAll();
4. 概念
本文档页面当前信息有限。 请參考Hibernate会话以掌握会话和identiry scope的完整概念。
查询和查询构建器
查询返回符合特定条件的实体。 在greenDAO中。你能够使用原始SQL来制定查询。或者使用QueryBuilder API,相对而言后者会更easy。
此外,查询支持懒载入结果,当在大结果集上操作时。能够节省内存和性能。
1. 查询构建器
编写SQL可能非常困难,而且easy出现错误。这些错误仅在运行时被察觉。 QueryBuilder类同意你为没有SQL的实体构建自己定义查询。并帮助在编译时检測错误。
简单条件演示样例:查询全部名为“Joe”的用户,按姓氏排序:
List<User> joes = userDao.queryBuilder()
.where(Properties.FirstName.eq("Joe"))
.orderAsc(Properties.LastName)
.list();
嵌套条件演示样例:获取1970年10月或之后出生的名为“Joe”的用户。
假设我们有一个用户的生日作为年,月和日的单独属性。 然后。我们能够用更正式的方式表示条件:名字是“Joe”AND(出生年份大于1970年或(出生年份是1970年,出生年份等于或大于10))( 10月,10月)。
QueryBuilder<User> qb = userDao.queryBuilder();
qb.where(Properties.FirstName.eq("Joe"),
qb.or(Properties.YearOfBirth.gt(1970),
qb.and(Properties.YearOfBirth.eq(1970), Properties.MonthOfBirth.ge(10))));
List<User> youngJoes = qb.list();
2. 限制。偏移和分页
有时候,你仅仅须要一个查询结果的子集,比如在用户界面中显示的前10个元素。 当拥有大量实体时,这是非常实用的(节省资源的),而且你也不能使用“where”语句来限制结果。
QueryBuilder 有定义限制和偏移的方法:
limit(int):限制查询返回的结果数。
offset(int):结合limit(int)设置查询结果的偏移量。 将跳过第一个偏移结果,而且结果的总数将受limit(int)限制。 你不能使用offset无限制(int)。
3. 使用自己定义类型作为參数
通常。greenDAO以透明方式映射查询中使用的类型。 比如。boolean被映射到INTEGER。具有0或1个值,而且Date被映射到(long)INTEGER值。
自己定义类型是一个例外:在构建查询时。总是必须使用数据库值类型。 比如。假设使用转换器将枚举类型映射到int值,则应在查询中使用int值。
4. 查询和LazyList
Query类表示能够多次运行的查询。当你使用QueryBuilder中的一个方法来获取结果(如list())时,运行过程中QueryBuilder 内部会使用Query 类。假设要多次运行同一个查询,应该在QueryBuilder 上调用build()来创建查询而并不是运行它。
greenDAO支持唯一结果(0或1个结果)和结果列表。
假设你想在Query (或QueryBuilder )上有唯一的结果调用unique()。这将为你提供单个结果或者在没有找到匹配的实体时返回null。
假设你的用例禁止null作为结果,调用uniqueOrThrow()将保证返回一个非空的实体(否则会抛出一个DaoException)。
假设希望多个实体作为查询结果,请使用下面方法之中的一个:
- list()全部实体都载入到内存中。结果一般是一个简单的ArrayList。
最方便使用。
- listLazy()实体按需载入到内存中。一旦列表中的元素第一次被訪问。它将被载入并缓存以备将来使用。使用完后必须关闭。
- listLazyUncached()一个“虚拟”实体列表:对列表元素的不论什么訪问都导致从数据库载入其数据。使用完后必须关闭。
- listIterator()让我们通过按需载入数据(lazily)来遍历结果。数据未缓存。使用完后必须关闭。
方法listLazy(),listLazyUncached()和listIterator()使用greenDAO的LazyList类。要按需载入数据。它保存对数据库游标的引用。
这就是为什么你必须确保关闭惰性列表和迭代器(通常在try / finally块)。
来自listLazy()的缓存延迟列表和listIterator()中的惰性迭代器在訪问或遍历全部元素后自己主动关闭游标。
假设列表处理过早停止,开发者须要自己调用close()进行处理
5. 多次运行查询
使用QueryBuilder构建查询后,这个Query对象是能够被反复使用的。 这比总是创建新的Query对象更高效。
假设查询參数不更改,你能够继续调用列表/唯一结果等方法进行查询操作。
可是,參数是能够被更改的:通过调用setParameter方法来更改參数。 当前,通过基于零的參数索引来寻址各个參数。 索引基于我们传入到QueryBuilder的顺序。 比如:
// fetch users with Joe as a first name born in 1970
Query<User> query = userDao.queryBuilder().where(
Properties.FirstName.eq("Joe"), Properties.YearOfBirth.eq(1970)
).build();
List<User> joesOf1970 = query.list();
// using the same Query object, we can change the parameters
// to search for Marias born in 1977 later:
query.setParameter(0, "Maria");
query.setParameter(1, 1977);
List<User> mariasOf1977 = query.list();
6. 在多线程中运行查询
假设在多个线程中使用查询,则必须调用forCurrentThread()获取当前线程的Query实例。 Query实例绑定到构建查询的那个线程。
你能够安全地设置Query对象的參数。不涉及到其它线程。
假设一个线程尝试改动查询參数或运行已经绑定到还有一个线程的查询绑,系统将抛出异常。 用这样的方式。你就不再须要synchronized语句了。 实际上,你应该避免锁定。由于假设并发事务使用同样的Query对象,这可能导致死锁。
每次调用forCurrentThread()时,查询參数将会被设置成使用构建器构建查询时的參数。
7. 原始查询
假设QueryBuilder不能满足你的需求。有两种方法来运行原始SQL并返回实体对象。 第一种,首选的方法是使用QueryBuilder和WhereCondition.StringCondition。
这样,你能够将不论什么SQL片段作为WHERE子句传递给查询构建器。
下面代码是一个理论演示样例,说明怎样运行子选择(使用连接将是更好的解决方式):
Query<User> query = userDao.queryBuilder().where(
new StringCondition("_ID IN " +
"(SELECT USER_ID FROM USER_MESSAGE WHERE READ_FLAG = 0)")
).build();
另外一种方法不使用QueryBuilder。 而是使用queryRaw或queryRawCreate方法。 它们同意您传递一个原始SQL字符串,它附加在SELECT和实体列之后。 这样,你能够有不论什么WHERE和ORDER BY子句来选择实体。
能够使用别名T来引用实体表。
下面演示样例显示怎样创建一个查询,该查询使用连接检索名为“admin”的组的用户(同样,greenDAO本身支持连接。这仅仅是为了演示):
Query<User> query = userDao.queryRawCreate(
", GROUP G WHERE G.NAME=?
AND T.GROUP_ID=G._ID", "admin"
);
注意:能够使用生成的常量引用表和列名称。 这是建议避免打字错误。由于编译器将检查名称。
在实体的DAO中。你将发现TABLENAME包括数据库表的名称。以及一个内部类属性,当中包括全部属性的常量(字段columnName)。
8. 删除查询
批量删除不会删除单个实体,可是全部实体都符合一些条件。
要运行批量删除,请创建一个QueryBuilder,调用其buildDelete()方法。并运行返回的DeleteQuery。
***API的这一部分能够在将来改变,比如能够加入方便的方法。
请注意,批量删除眼下不影响identity scope中的实体,比如。假设已删除的实体先前已缓存,并通过其ID(载入方法)进行訪问,则能够“复活”。 请考虑马上清除身份范围。假设这可能会导致你的用例的问题。*
9. 疑难解答查询
您的查询不返回预期结果? 在QueryBuilder上启用SQL和參数记录有两个静态标志:
QueryBuilder.LOG_SQL = true;
QueryBuilder.LOG_VALUES = true;
当调用当中一个构建方法时,它们将记录生成的SQL命令和传递的值,并将它们与实际所需的值进行比較。
此外,它可能有助于将生成的SQL拷贝到一些SQLite数据库浏览器,并看看它怎样运行。
查询构建器连接表
1. 连接
非普通查询通常须要几个实体类型(表)的数据。 在SQL世界中,能够通过使用连接条件“连接”两个或多个表来实现。
让我们考虑一个实体User。它与Address实体具有一对多的关系。 然后。我们要查询居住在“Sesame Street”上的用户:我们必须使用用户ID与User实体加入Address实体,并在Address实体上定义WHERE条件:
QueryBuilder<User> queryBuilder = userDao.queryBuilder();
queryBuilder.join(Address.class, AddressDao.Properties.userId)
.where(AddressDao.Properties.Street.eq("Sesame Street"));
List<User> users = queryBuilder.list();
连接须要目标实体类作为每一个实体的參数和连接属性。 在演示样例中,仅仅定义Address实体的join属性,由于默认使用主键属性。 换句话说。查询导致用户具有UserId等于User实体ID而且还具有特定街道的Address实体。
2. 查询构建器连接API
由于能够在使用主键属性时省略join属性,因此QueryBuilder中提供了三种重载的连接方法:
/**
* Expands the query to another entity type by using a JOIN.
* The primary key property of the primary entity for
* this QueryBuilder is used to match the given destinationProperty.
*/
public <J> Join<T, J> join(Class<J> destinationEntityClass, Property destinationProperty)
/**
* Expands the query to another entity type by using a JOIN.
* The given sourceProperty is used to match the primary
* key property of the given destinationEntity.
*/
public <J> Join<T, J> join(Property sourceProperty, Class<J> destinationEntityClass)
/**
* Expands the query to another entity type by using a JOIN.
* The given sourceProperty is used to match the given
* destinationProperty of the given destinationEntity.
*/
public <J> Join<T, J> join(Property sourceProperty, Class<J> destinationEntityClass,
Property destinationProperty)
3. 链式连接
此外。greenDAO同意跨多个表连接。 在这里,使用还有一个连接和目标实体定义连接。 在这样的情况下,第一连接的目的实体变为第二联接的起始实体。
用于链接连接的QueryBuilder API例如以下所看到的:
/**
* Expands the query to another entity type by using a JOIN.
* The given sourceJoin's property is used to match the
* given destinationProperty of the given destinationEntity.
* Note that destination entity of the given join is used
* as the source for the new join to add. In this way,
* it is possible to compose complex "join of joins" across
* several entities if required.
*/
public <J> Join<T, J> join(Join<?, T> sourceJoin, Property sourceProperty,
Class<J> destinationEntityClass, Property destinationProperty)
让我们看看还有一个有三个实体的样例:城市。国家和大陆。 假设我们要查询欧洲全部人口至少100万的全部城市。它将例如以下所看到的:
QueryBuilder<City> qb = cityDao.queryBuilder().where(Properties.Population.ge(1000000));
Join country = qb.join(Properties.CountryId, Country.class);
Join continent = qb.join(country, CountryDao.Properties.ContinentId,
Continent.class, ContinentDao.Properties.Id);
continent.where(ContinentDao.Properties.Name.eq("Europe"));
List<City> bigEuropeanCities = qb.list();
4. 自连接/树演示样例
连接也能够与引用单个实体的关系一起使用。 比如,我们想找到全部的人,他的祖父的名字是“林肯”。 让我们假设我们有一个具有指向同一个实体的fatherId属性的Person实体。 然后,查询构建例如以下:
QueryBuilder<Person> qb = personDao.queryBuilder();
Join father = qb.join(Person.class, Properties.FatherId);
Join grandfather = qb.join(father, Properties.FatherId, Person.class, Properties.Id);
grandfather.where(Properties.Name.eq("Lincoln"));
List<Person> lincolnDescendants = qb.list();
如您所见。连接是构建跨多个实体类型或关系的查询的强大工具。
实体之间的关系(一对多)
1. 关系
数据库表能够使用1:1,1:N或N:M关系彼此相关。 假设你刚接触数据库关系,你须要在我们讨论ORM之前快速的恶补一下。 这里有一些关于数据库关系的网络链接。
在greenDAO中,实体使用一对一或一对多关系。 比如,假设要在greenDAO中建模1:n关系,则将具有一对一关系和一对多关系。 可是,请注意。一对一和一对多关系不会彼此连接。因此你必须更新两者。
2. 构建一对一关系
@ToOne注解定义与还有一个实体(一个实体对象)的关系。 将其应用于包括其它实体对象的属性。
在内部。greenDAO须要一个指向由joinProperty參数指定的目标实体的ID的附加属性。
假设此參数不存在,则会自己主动创建一个附加列来保存这个信息。
@Entity
public class Order {
@Id private Long id;
private long customerId;
@ToOne(joinProperty = "customerId")
private Customer customer;
}
@Entity
public class Customer {
@Id private Long id;
}
一对一关系的getter方法(在此演示样例中为getCustomer())在其第一次调用时延迟解析目标实体。
兴许调用将马上返回先前解析的对象。
注意,假设更改外键属性(这里为customerId),下一次对getter(getCustomer())的调用将解析实体以获取更新的ID。
另外,假设设置了一个新实体(setCustomer()),外键属性(customerId)也将被更新。
Customer customerA = user.getCustomer();
// change the customer id
user.setCustomerId(customerIdB);
// or set a customer with a different id
user.setCustomer(customerB);
customerB = user.getCustomer();
assert(customerA.getId() != customerB.getId());
注意:要热载入一对一关系,请使用实体DAO类的loadDeep()和queryDeep()。 这将解析与单个数据库查询具有全部一对一关系的实体。 假设你总是訪问相关实体。这将有助于性能的提高。
2. 构建一对多关系
@ToMany定义与一组其它实体(多个实体对象)的关系。
将此应用于表示目标实体列表的属性。
引用的实体必须有一个或多个属性指向拥有@ToMany的实体。
有三种可能性来指定关系映射,仅仅使用当中一个就可以:
- referencedJoinProperty參数:指定目标实体中指向此实体标识的“外键”属性的名称。
@Entity
public class Customer {
@Id private Long id;
@ToMany(referencedJoinProperty = "customerId")
@OrderBy("date ASC")
private List<Order> orders;
}
@Entity
public class Order {
@Id private Long id;
private Date date;
private long customerId;
}
- joinProperties參数:对于更复杂的关系,你能够指定一列@JoinProperty注解。 每一个@JoinProperty须要原始实体中的源属性和目标实体中的引用属性。
@Entity
public class Customer {
@Id private Long id;
@Unique private String tag;
@ToMany(joinProperties = {
@JoinProperty(name = "tag", referencedName = "customerTag")
})
@OrderBy("date ASC")
private List<Site> orders;
}
@Entity
public class Order {
@Id private Long id;
private Date date;
@NotNull private String customerTag;
}
- @JoinEntity:假设你正在运行涉及还有一个连接实体/表的N:M(多对多)关系,请在属性上放置此附加注解。
@Entity
public class Product {
@Id private Long id;
@ToMany
@JoinEntity(
entity = JoinProductsWithOrders.class,
sourceProperty = "productId",
targetProperty = "orderId"
)
private List<Order> ordersWithThisProduct;
}
@Entity
public class JoinProductsWithOrders {
@Id private Long id;
private Long productId;
private Long orderId;
}
@Entity
public class Order {
@Id private Long id;
}
一旦运行,插件将生成一个getter来解析被引用实体的列表。 比如在前两种情况下:
// return all orders where customerId == customer.getId()
List<Order> orders = customer.getOrders();
3. 解析和更新对多关系
多对多关系在第一个请求上解析比較慢,然后将List对象缓存在实体中。 因此,兴许採用get方法调用将不会查询数据库。
更新多对多关系须要一些额外的工作。 由于缓存了一对多列表。所以当将相关实体加入到数据库时,它们不会更新。 下面代码说明了这样的行为:
// get the current list of orders for a customer
List<Order> orders1 = customer.getOrders();
// insert a new order for this customer
Order order = new Order();
order.setCustomerId(customer.getId());
daoSession.insert(order);
// get the list of orders again
List<Order> orders2 = customer.getOrders();
// the (cached) list of orders was not updated
// orders1 has the same size as orders2
assert(orders1.size() == orders2.size);
// orders1 is the same object as orders2
assert(orders1.equals(orders2));
因此,要加入新的相关实体,请将它们手动加入到源实体的多对多列表中。例如以下:
// get the to-many list before inserting the new entity
// otherwise the new entity might be in the list twice
List<Order> orders = customer.getOrders();
// create the new entity
Order newOrder = ...
// set the foreign key property
newOrder.setCustomerId(customer.getId());
// persist the new entity
daoSession.insert(newOrder);
// add it to the to-many list
orders.add(newOrder);
同样。你能够删除相关实体,例如以下:
List<Order> orders = customer.getOrders();
// remove one of the orders from the database
daoSession.delete(someOrder);
// manually remove it from the to-many list
orders.remove(someOrder);
加入。更新或删除很多相关实体时,能够使用重置方法清除缓存的列表。
然后下一个get将又一次查询相关实体:
// clear any cached list of related orders
customer.resetOrders();
List<Order> orders = customer.getOrders();
4. 双向1:N关系
有时你想在两个方向上建立1:N关系。 在greenDAO中,你必须加入一对一和一对多关系才干实现此目的。
下面演示样例显示了客户和订单实体的完整建模。我们之前用作演示样例。 这次。我们使用customerId属性创建两个关系:
@Entity
public class Customer {
@Id private Long id;
@ToMany(referencedJoinProperty = "customerId")
@OrderBy("date ASC")
private List<Order> orders;
}
@Entity
public class Order {
@Id private Long id;
private Date date;
private long customerId;
@ToOne(joinProperty = "customerId")
private Customer customer;
}
让我们假设我们有一个订单实体。
使用这两种关系,我们能够得到客户和客户所做的全部订单:
List<Order> allOrdersOfCustomer = order.getCustomer().getOrders();
5. 演示样例:建模树关系
您能够通过使用指向自身的一对一和多对一关系建模实体来建模树关系:
@Entity
public class TreeNode {
@Id private Long id;
private Long parentId;
@ToOne(joinProperty = "parentId")
private TreeNode parent;
@ToMany(referencedJoinProperty = "parentId")
private List<TreeNode> children;
}
生成的实体同意您导航其父级和子级:
TreeNode parent = entity.getParent();
List<TreeNode> children = entity.getChildren();
6. 很多其它演示样例
查看DaoExample项目以获取完整的Android应用演示样例。
此外。DaoTestEntityAnnotation项目附带了几个关系測试。 当中,除了其它演示样例和測试项目之外。能够用作进一步的演示样例。
自己定义类型:将类和枚举映射到数据库值
自己定义类型同意实体具有不论什么类型的属性。 默认情况下,greenDAO支持下面类型
boolean, Boolean
int, Integer
short, Short
long, Long
float, Float
double, Double
byte, Byte
byte[]
String
Date
1. 转换注解和属性转换器
要加入对自己定义类型的支持,能够使用@Convert凝视将它们映射到当中一种受支持的类型。
同一时候你还须要提供PropertyConverter实现。
比如,你能够使用自己定义Color类在实体中定义颜色,并将其映射到整数。
或者你能够将流行的org.joda.time.DateTime从Joda Time映射到Long。
这是将枚举映射到整数的演示样例:
@Entity
public class User {
@Id
private Long id;
@Convert(converter = RoleConverter.class, columnType = Integer.class)
private Role role;
public enum Role {
DEFAULT(0), AUTHOR(1), ADMIN(2);
final int id;
Role(int id) {
this.id = id;
}
}
public static class RoleConverter implements PropertyConverter<Role, Integer> {
@Override
public Role convertToEntityProperty(Integer databaseValue) {
if (databaseValue == null) {
return null;
}
for (Role role : Role.values()) {
if (role.id == databaseValue) {
return role;
}
}
return Role.DEFAULT;
}
@Override
public Integer convertToDatabaseValue(Role entityProperty) {
return entityProperty == null ? null : entityProperty.id;
}
}
}
注意:假设你在实体类中定义自己定义类型或转换器,它们必须是静态的。
不要忘记正确处理空值 - 通常,假设输入为null,则应返回null。
转换器的数据库类型意义上不是SQLite类型,而是由greenDAO提供的原始Java类型。
建议使用易于转换的基本类型(int,long。byte数组,String。…)。
注意:为了获得最佳性能。greenDAO将为全部转换使用单个转换器实例。 确保转换器除了无參数默认构造函数之外没有不论什么其它构造函数。
另外,使它线程安全,由于它可能在多个实体上并发调用。
2. 怎样正确转换枚举
枚举在像实体这样的数据对象中使非经常常使用的。 当持久化枚举时,有下面几个不错的方法:
- 不要持久化枚举的序号或名称:两者都不稳定,而且非常有可能下次编辑枚举定义的时候就变化了。
- 使用stable ids:在你的枚举中定义一个保证稳定的自己定义属性(整数或字符串)。
使用它来进行持久性映射。
- 未知值:定义一个UNKNOWN枚举值。 它能够用于处理空值或未知值。 这将同意你处理像旧的枚举值被删除而不会崩溃您的应用程序的情况。
3. 自己定义类型在查询中的处理
QueryBuilder不知道自己定义类型。 您必须对查询使用原语类型(比如在WHERE參数中)。
还要注意。在数据库中完毕的操作总是引用原始类型。比如在ORDER BY子句中。
数据库加密
greenDAO支持加密的数据库来保护敏感数据。
尽管较新版本号的Android支持文件系统加密,但Android本身并不为数据库文件提供加密。 因此,假设攻击者获得对数据库文件的訪问(比如通过利用安全缺陷或欺骗有根的设备的用户),攻击者能够訪问该数据库内的全部数据。
使用受密码保护的加密数据库添加了额外的安全层。 它防止攻击者简单地打开数据库文件。
1. 使用自己定义SQLite构建
由于Android不支持加密数据库,所以你须要在APK中捆绑SQLite的自己定义构建。
这些定制构建包括CPU相关和本地代码。
所以你的APK大小将添加几个MByte。 因此,你应该仅仅使用加密,假设你真的须要它。
2. 设置数据库加密
greenDAO直接支持带有绑定的SQLCipher。
SQLCipher是使用256位AES加密的SQLite的自己定义构建。
2.1 加入SQLCipher依赖关系
请參阅SQLCipher for Android。了解怎样向项目加入SQLCipher。
2.2 数据库初始化
确保使用DaoMaster中提供的OpenHelper的子类来创建数据库实例。
比如简单的DevOpenHelper,也在DaoMaster中提供。
然后,在创建数据库实例时,仅仅需调用.getEncryptedWritableDb()而不是.getWritableDb()。
最后,像往常一样将数据库传递给DaoMaster:
DevOpenHelper helper = new DevOpenHelper(this, "notes-db-encrypted.db");
Database db = helper.getEncryptedWritableDb("<your-secret-password>");
daoSession = new DaoMaster(db).newSession();
DevOpenHelper helper = new DevOpenHelper(this, "notes-db-encrypted.db");
Database db = helper.getEncryptedWritableDb("<your-secret-password>");
daoSession = new DaoMaster(db).newSession();
3. 数据库抽象
greenDAO对全部数据库交互使用一个小型的抽象层,因此支持标准和非标准的SQLite实现:
- Android的标准android.database.sqlite.SQLiteDatabase
- SQLCipher的net.sqlcipher.database.SQLiteDatabase
- 不论什么SQLite兼容的数据库,它能够实现
org.greenrobot.greendao.database.Database(比如SQLite的自己定义构建)
这使您能够轻松地从标准数据库切换到加密数据库,由于在针对DaoSession和单个DAO时,代码将是同样的。
3.1 使用Robolectric进行单元測试
数据库抽象同意用Robolectric进行单元測试。
即使你的应用程序使用加密的数据库。你的測试也能够使用未加密的数据库。
Robolectric实现标准的SQLite API。没有办法载入自己定义SQLite构建(Android二进制)。 因此,对于你的測试。在创建数据库实例时使用.getWritableDb()而不是.getEncryptedWritableDb()(见上文)。
3.2 SQLCipher的已知问题
SQLCipher是SQLite的自己定义构建。 它的Android API与Android系统API有点分歧。
这里是我们注意到的问题(与greenDAO无关):
- 抛出的异常不是类型android.database.SQLException(请參阅SQLcipher问题223)
- SQLCipher比近期的SQLiteDatabase实现锁定很多其它。 这不仅减少了并发性,而且添加了死锁的机会。
- 从SQLCipher 3.5.0開始,缺少Android归类(如LOCALIZED)
写在最后
个人翻译,用于后面查阅。若有不当请担待~
原文链接:
http://greenrobot.org/greendao/