greenDAO 是一个将对象映射到 SQLite 数据库中的轻量且快速的 ORM 解决方案。它的本质就是提供一个面向对象的接口,使得开发者更加方便地将数据存储到数据库SQLite之中。我们只需要定义数据模型,greenDAO就会为我们生成实体类以及DAOs(data access objects),(在3.0之后不需要我们编写generator而是编写实体类并添加注解,greenDAO会为我们生成schema,以及DAOs)从而避免了开发者编写较多枯燥的数据存储和加载的代码。此外greenD还提供了一些高级功能,比如对话缓存(session cache),eager loading, active entities(这些还不懂)。
在greenDa中,默认会为每一个实体类建立一张数据表,实体类的每一个属性对应数据表中的一列。
本篇文章算是对greenDao官方文档的一个简单翻译,还是有很多不懂之处,写在这里仅供自己以后使用中做一个参考,其中掺杂一点自己的理解,可能会有不当之处,有错误之处请批评指正!
1. 基本使用方法
1. 配置
添加依赖:
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.greenrobot:greendao-gradle-plugin:3.0.0'
}
}
apply plugin: 'org.greenrobot.greendao'
dependencies {
compile 'org.greenrobot:greendao:3.0.1'
}
2. Entities
从3.0版本开始,greenDao不再需要编写Generator类,而是需要开发者编写带有注解的Entities实体类,该实体类就是需要持久化存储在数据库中的类。简单实例如下:
@Entity
public class User {
@Id
private Long id;
private String name;
@Generated(hash = 873297011)
public User(Long id, String name) {
this.id = id;
this.name = name;
}
@Generated(hash = 586692638)
public User() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@Entity注解可以表明该类是需要持久化的类,build->Make Project之后,会在build/generated/source/greendao下生成DAOMaster,DAOSession,实体类以及对应的DAO classes,在此例中生成DaoMaster, DaoSession, 和UserDao,使用这三个类就可以执行创建数据库,以及对数据库执行关于实体类的增删改查。
3. 数据库操作
public class MainActivity extends AppCompatActivity {
UserDao userDao;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DaoMaster.DevOpenHelper helper = new DaoMaster.DevOpenHelper(this, "recluse-db", null);
SQLiteDatabase db = helper.getWritableDatabase();
DaoMaster daoMaster = new DaoMaster(db);
DaoSession daoSession = daoMaster.newSession();
userDao = daoSession.getUserDao();
User user = new User((long)1, "recluse");
userDao.insert(user);
userDao.update(user);
userDao.deleteByKey((long)1);
userDao.delete(user);
}
}
如上例所示,通过DaoMaster, DaoSession, UserDao,就可以对数据进行增删改查操作了,其中查询是使用较多的操作,greenDAO中专门提供了Query和QueryBuilder类,便于查询的操作和优化。后面对此进行详细介绍。以上就是greenDao的基本使用步骤,就可以实现实体对象在数据库的持久化存储了。
2. 详细介绍
1. 实体类的注解
在Java代码中使用实体类代表我们需要持久化的数据。之前使用greenDAO是由开发者编写generator,定义schema并添加实体类的名称以及属性,由greenD为我们生成实体类。而在3.0以后是由我们编写实体类,并添加注解,由greenDAO识别schema并生成相应的DAOs。以下为注解介绍。
- @Entity,该实例是类的注解,告诉greenDAO这个类是需要持久化的实体类。此外它还可以配置一些参数,如下:
@Entity(
// If you want to 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
)
public class User {
...
}
- @ID, field注解, 表示选择一个long或Long类型的属性作为该实体所对应数据库中数据表的主键,参数可以设置(autoincreament=true)
- @Property field 注解,可以自定义该属性在数据表的列名,默认的列名为属性名大写,并由下划线隔开多个单词,@Property(nameInDb=”XXXX”)可以自定义列名。
- @Transient 与Java中的Transient关键字类似,指不对该属性持久化,即不为该属性在数据表中创建相应列。
- @Index 为属性创建索引,有name和unique两个参数,name可以为索引指定名称,unique与数据表中建立索引的unique含义一致
- @Unique 含义与数据表中列的unique一致, 这种情况下,SQLite会自动为该列建立索引。
- @Generated greenDAO会根据开发者定义的实体类定义schema,并生成DAOs,而在生成过程中会为开发者编写的实体类的一些方法和域上添加@Generated注解,该注解只是提示开发者,避免对此域或方法代码的修改,如果想手动对其修改则需要改成@Keep注解,但是会影响后面的持久化过程。最好不要这样做。
此外还有@ToOne, @ToMany, @JoinEntity与多个数据表的关系有关,后面再对此详细介绍。
- @Generated greenDAO会根据开发者定义的实体类定义schema,并生成DAOs,而在生成过程中会为开发者编写的实体类的一些方法和域上添加@Generated注解,该注解只是提示开发者,避免对此域或方法代码的修改,如果想手动对其修改则需要改成@Keep注解,但是会影响后面的持久化过程。最好不要这样做。
2. Sessions
greenDAO为我们生成的类中包括DaoMaster,DaoSession和一些实体对应的DAOs,而前两个是greenDAO的机制中核心的接口。
首先每一个DaoMaster持有一个数据库连接,通过DaoMaster#newSession()方法可以实例化多个Session,这些Session对应同一个数据库连接,但是系统会为每一个Session分配内存,在这片内存中会为实体进行缓存。每一个Session对应一个Identity scope(这个名称空间不太懂,官方文档中提供了一个Hibernate中介绍Session和Identity Scope概念的链接)。
从DaoSession中可以获取各实体类的对应DAO,然后就可以进行增删改查的操作了,对于每一个Session中的查询操作都会对查到的实体类做缓存操作,所以对应同一个Session的多次查询操作,如果entity的对象在该Session中有对应缓存则直接使用,而不再从数据库中读取数据并构建新的实体类对象。
3. 查询
数据库的查询是最为频繁的操作,greenDAO对查询提供了更为方面的操作API和优化,此外greenDAO也支持纯SQL语句。但是最好使用greenDAO提供的QueryBuilder API,使用更为简单方面而且支持惰性加载。
QueryBuilder的基本使用方法
通过DAOs的queryBuilder()方法构建一个构建器对象,然后对该构建器设置查询,排序条件等,最后获取查询结果。如官方文档给出的实例:
QueryBuilder qb = userDao.queryBuilder(); //获取QueryBuilder
qb.where(Properties.FirstName.eq("Joe")) //设置查询条件
.orderAsc(Properties.LastName) //设置排序
.list(); //返回查询结果
where()方法中可以添加多个查询条件,即WhereCondition对象,多个条件取与的关系,也可以使用whereOr()方法,多个条件取或的关系,不过该方法至少有两个条件才可以。
另外qb对象还有or(), and()等方法连接多个条件,返回类型是WhereCondition, 从而可以组合出我们想要的查询条件。
这里注意Properties是userDao的一个静态内部类,负责持有各个属性,每个属性都是一个Property类型,该类型的对象都有like(), in()等操作方法,这部分在后面介绍生成的DAO部分再做介绍。
如下官方文档示例:
QueryBuilder 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 youngJoes = qb.list();
返回以下条件的结果:First name is “Joe” AND (year of birth is greater than 1970 OR (year of birth is 1970 AND month of birth is equal to or greater than 10 (October).
在有些情况下,可能只需要满足查询条件的一部分结果,这时候where()方法就无能为力了,greenDAO提供了limit(int)和offset(int)方法,设置返回结果的数量以及在所有结果中的偏移,返回结果时只构建这一部分数据对应的实体类对象,可以用于只返回满足条件的某一部分结果,从而节省内存。
greenDAO提供Query类用于返回查询结果,通过QueryBuilder#build()方法返回这一次查询的Query对象,Query对象可以返回单个结果或者所有结果。
返回单个结果:使用unique()或uniqueOrThrow(),返回单个结果,如果没有满足条件的结果,前者返回null, 后者抛出异常
返回所有结果:使用list(): 在内存中构建所有满足条件的实体类对象,通常保存到ArrayList中(with no magic involved 不明白什么意思);listLazy(): 当其中的一个实体类对象使用时才加载,会缓存下来,之后可以继续使用;listLazyUncached():即不缓存的惰性加载;listIterator():也是惰性加载,返回一个迭代器,可以方便地遍历查询结果,没有缓存。
返回所有结果的后面三个方法都是用了greenDao的LazyList类,它是持有数据库的一个Cursor对象,所以在使用完以后需要关闭,释放空间。listLazy()和listIterator()方法返回的结果被遍历完,即所有结果都使用过以后会自动关闭cursor,但是如果有中途就结束的可能这应该由开发者负责关闭。
除此以外,如果不需要多次查询的情况下,可以直接通过queryBuilder调用这些返回结果的方法,无需Query对象,其实内部也是通过Query实现的,只不过节俭了代码量。
- 多次查询
返回的Query对象在没有改变查询条件的情况下可以多次查询,避免了多次构建Query对象,如果更查询条件可以调用setParameter()方法,如下官方实例:
- 多次查询
Query query = userDao.queryBuilder().where(
Properties.FirstName.eq("Joe"), Properties.YearOfBirth.eq(1970))
.build();
List joesOf1970 = query.list();
这里想要更改这两个条件
query.setParameter(0, "Maria");
query.setParameter(1, 1977);
List mariasOf1977 = query.list();
即通过条件的序号更改查询条件。
如果在多线程中做查询操作,需要调用Query的forCurrentThread()返回Thread Local的Query对象。通过该对象可以执行线程安全的查询,无需锁(使用锁可能会导致死锁的可能,因为事务的原因)。在该线程中可以修改该查询对象的条件,而其他线程如果想要修改该查询对象则会抛出异常,保证了该Query对象在该线程中的安全性。
greenDAO支持SQL查询,可以使用where语句构建StringCondition,也可以执行queryRaw()或queryRawCreate(),如下官方实例:
Query query = userDao.queryBuilder().where(
new StringCondition("_ID IN " +
"(SELECT USER_ID FROM USER_MESSAGE WHERE READ_FLAG = 0)")).build();
这个由字符串构建条件语句
Query query = userDao.queryRawCreate(
", GROUP G WHERE G.NAME=? AND T.GROUP_ID=G._ID", "admin");
SQL语句的字符串为SELECT和Entities的列名之后的SQL语句
4. 连接
通常数据查询都需要多个数据表,也就是需要数据表的连接。首先看一个官方的实例,该实例可以查询住在某个位置的所有用户:
QueryBuilder
queryBuilder = userDao.queryBuilder(); queryBuilder.join(Address.class, AddressDao.Properties.userId) .where(AddressDao.Properties.Street.eq("Sesame Street")); List
users = queryBuilder.list();
QueryBuilder Join API
由于可以默认连接某个数据表的主键,所以可以省略连接源或目的的属性,因此join()方法有如下三种重载形式:
/**
* 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
Join
join(Class
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
Join
join(Property sourceProperty, Class
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
Join
join(Property sourceProperty, Class
destinationEntityClass, Property destinationProperty)
链式连接
/**
* 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
Join
join(Join
sourceJoin, Property sourceProperty, Class
destinationEntityClass, Property destinationProperty)
以下是官方实例:涉及三个实例City, Country, and Continent.查询欧洲的所有人数超过一百万的所有城市:
QueryBuilder 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
bigEuropeanCities = qb.list();
自连接
官方实例,查找所有人当中爷爷的名字为:”Lincoln”的
QueryBuilder 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
lincolnDescendants = qb.list();
5. Relations
在建立数据库时,每一个实体类会建立一张数据表,代表一个关系,而不同实体之间必然存在一定的关系,反映到数据表上也需要建立关系。
比如一个用户的账户都有对应的头像picture,且假设每个用户的头像Picture对象都不同,因此每个picture也对应一个用户,这就是一对一的关系,而在网上购物可以下订单,每个用户可以有多个订单Oder,而每个Oder都对应一个用户User,这是一对多的关系。在数据表的关系中可以通过外键表示这种一对一和一对多的关系。除此以外还有多对多的关系,在对象的结构中每个Oder包含多个物品Item,而每个Item也可能包含在多个Oder中,因此这是多对多的关系,在数据表中需要额外的表表示对应关系,如Oder_Item表,表示一个Oder与一个Item具有关系。
在greenDao中,使用@ToOne表示一对一的关系,使用@ToMany表示一对多的关系,而多对多的关系支持度不够好,目前还不完善,使用较为复杂。
- @ToOne
比如每个用户都有一个对应Picture属性,需要一个pictureId代表这个Picture属性,通过@ToOne(joinProperty = “XXXX”)指定pictureId,在数据表中则会有pictureId这一列作为外键,与Picture数据表建立联系,如果你没有指定pictureId, greenDAO也会在数据表中生成一列属性其作用与指定的pictureId相同,而实体类中则可以使用User的Picture属性,代码如下:
- @ToOne
@Entity
public class User {
@Id
private Long id;
private String name;
private Long pictureId;
@ToOne(joinProperty = "pictureId")
private Picture picture;
....
}
当然greenDAO会为我们生成其他的代码,如构造器,getter和setter等,在代码中可以直接使用user.getPicture()和setPicture,而在数据表中则是pictureId是指向Picture数据表的外键。Picture的实体类如下:
@Entity
public class Picture {
@Id
private long pictureId;
...
}
- @ToMany
一对多的关系,定义了一个实体对象对应着多个实体对象,比如一个用户对应多个Order, 在建立数据表示会在目标实体(即一对多的那个多的实体类)的数据表中建立外键,指向源实体类(一对多中的一那个实体类)的数据表。目标数据表中的外键属性由@ToMany(referencedJoinProperty = “XXXX”)指定。
- @ToMany
如User中添加List orders属性:
@Entity
public class User {
@Id
private Long id;
private String name;
private Long pictureId;
@ToOne(joinProperty = "pictureId")
private Picture picture;
@ToOne
private Picture thumbnailPicture;
@ToMany(referencedJoinProperty = "ownerId")
private List
oders; ... }
Order的实例代码为:
@Entity
public class Order {
@Id
private long id;
private long ownerId;
...
}
在build->Make Project之后会生成对应代码,此时在代码中可以使用User#getOrders()获取user的所有对应的order的List. 在这种情况下, ownerId是在Order中的外键,指向User的主键Id。
另外,对于较为复杂的关系可以使用joinProperties属性,其值可以设置一系列的@JoinProperty(name = “XXXX”, referencedName = “YYYY”),可以建立目标实体类中YYYY属性指向源实体类XXXX的属性,其中YYYY为非空,XXXX为unique的,不一定是主键,这样的关系可以有多个。 如下为官方实例:
@Entity
public class User {
@Id private Long id;
@Unique private String authorTag;
@ToMany(joinProperties = {
@JoinProperty(name = "authorTag", referencedName = "ownerTag")
})
private List
ownedSites; } @Entity public class Site { @Id private Long id; @NotNull private String ownerTag; }
3. 生成代码的解析
3.0以后实体类需要开发者编写并使用注解,greenDAO会根据注解解析schema,并生成一系列的类,主要包括DAOMaster,DAOSession以及相应的DAOs
1. DAOMaster
/**
* Master of DAO (schema version 1): knows all DAOs.
*/
public class DaoMaster extends AbstractDaoMaster {
public static final int SCHEMA_VERSION = 1;
/** Creates underlying database table using DAOs. */
public static void createAllTables(Database db, boolean ifNotExists) {
OrderDao.createTable(db, ifNotExists);
PictureDao.createTable(db, ifNotExists);
UserDao.createTable(db, ifNotExists);
}
/** Drops underlying database table using DAOs. */
public static void dropAllTables(Database db, boolean ifExists) {
OrderDao.dropTable(db, ifExists);
PictureDao.dropTable(db, ifExists);
UserDao.dropTable(db, ifExists);
}
/**
* WARNING: Drops all table on Upgrade! Use only during development.
* Convenience method using a {@link DevOpenHelper}.
*/
public static DaoSession newDevSession(Context context, String name) {
Database db = new DevOpenHelper(context, name).getWritableDb();
DaoMaster daoMaster = new DaoMaster(db);
return daoMaster.newSession();
}
public DaoMaster(SQLiteDatabase db) {
this(new StandardDatabase(db));
}
public DaoMaster(Database db) {
super(db, SCHEMA_VERSION);
registerDaoClass(OrderDao.class);
registerDaoClass(PictureDao.class);
registerDaoClass(UserDao.class);
}
public DaoSession newSession() {
return new DaoSession(db, IdentityScopeType.Session, daoConfigMap);
}
public DaoSession newSession(IdentityScopeType type) {
return new DaoSession(db, type, daoConfigMap);
}
/**
* Calls {@link #createAllTables(Database, boolean)} in {@link #onCreate(Database)} -
*/
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);
}
@Override
public void onCreate(Database db) {
Log.i("greenDAO", "Creating tables for schema version " + SCHEMA_VERSION);
createAllTables(db, false);
}
}
/** WARNING: Drops all table on Upgrade! Use only during development. */
public static class DevOpenHelper extends OpenHelper {
public DevOpenHelper(Context context, String name) {
super(context, name);
}
public DevOpenHelper(Context context, String name, CursorFactory factory) {
super(context, name, factory);
}
@Override
public void onUpgrade(Database db, int oldVersion, int newVersion) {
Log.i("greenDAO", "Upgrading schema from version " + oldVersion + " to " + newVersion + " by dropping all tables");
dropAllTables(db, true);
onCreate(db);
}
}
}
首先看最后的两个静态内部类,在Android系统中为了便于对SQLiteDatabase做数据库操作提供了SQLiteOpenHelper, 这个帮助类是一个抽象方法,有两个抽象方法分别是onCreate()和onUpdate()分别在数据库建立和升级的时候回调。而在DAOMaster中添加了OpenHelper继承了DatabaseOpenHelper(DatabaseOpenHelper继承了SQLiteOpenHelper, 并保留了那两个抽象方法),并实现了onCreate()方法,并在该方法中调用createAllTables(db, false)创建所有的数据表。而DevOpenHelper则继续继承OpenHelper并实现onUpdate()方法,在方法中删除所有数据表并重新创建。createAllTables和dropAllTables方法比较简单,只是简单地调动DAOs的数据表创建和删除方法。
然后是DAOMaster的构造器,在构造器中注册数据表的实体类, AbstractDAOMaster#registerDaoClass()的代码如下:
protected void registerDaoClass(Class
> daoClass) {
DaoConfig daoConfig = new DaoConfig(db, daoClass);
daoConfigMap.put(daoClass, daoConfig);
}
就是创建DaoConfig对象,注册一个实体类,并添加到DAOMaster的daoConfigMap对象中,以Map的形式保存信息,表示该实体类需要持久化,为其创建数据表。
接着就是两个重要方法newSession()的两个重载方法,每一个DaoMaster对象代表一个数据库的连接,而返回一个新的Session就代表一个会话,通过同一个会话中的DAOs进行的数据库操作,greenDao会对其优化,如查询操作会对结果缓存,避免每次都要从数据库中国读取数据并多次建立对象。
最后是一个静态方法newSession()是一个便捷方法,在该方法中可以看出就是新建一个数据库,并实例化一个master并返回一个new Session。与在基本使用方法中介绍的步骤完全一致,只不过这样一个静态方法集成了这几个步骤,使用更为方便而已。
2. DaoSession
public class DaoSession extends AbstractDaoSession {
private final DaoConfig orderDaoConfig;
private final DaoConfig pictureDaoConfig;
private final DaoConfig userDaoConfig;
private final OrderDao orderDao;
private final PictureDao pictureDao;
private final UserDao userDao;
public DaoSession(Database db, IdentityScopeType type, Map
>, DaoConfig> daoConfigMap) { super(db); orderDaoConfig = daoConfigMap.get(OrderDao.class).clone(); orderDaoConfig.initIdentityScope(type); pictureDaoConfig = daoConfigMap.get(PictureDao.class).clone(); pictureDaoConfig.initIdentityScope(type); userDaoConfig = daoConfigMap.get(UserDao.class).clone(); userDaoConfig.initIdentityScope(type); orderDao = new OrderDao(orderDaoConfig, this); pictureDao = new PictureDao(pictureDaoConfig, this); userDao = new UserDao(userDaoConfig, this); registerDao(Order.class, orderDao); registerDao(Picture.class, pictureDao); registerDao(User.class, userDao); } public void clear() { orderDaoConfig.getIdentityScope().clear(); pictureDaoConfig.getIdentityScope().clear(); userDaoConfig.getIdentityScope().clear(); } public OrderDao getOrderDao() { return orderDao; } public PictureDao getPictureDao() { return pictureDao; } public UserDao getUserDao() { return userDao; } }
DaoSession主要工作就是提供DAOs用于数据库操作,首先看构造器,每个DaoSession会从DaoMaster中克隆所有实体类的DaoConfig,并初始化Identity Scope(这个概念目前还不懂), 而且会在构造器中初始化各个实体类的Dao class,并注册Dao class。AbstractDaoSession#registerDao()的代码如下:
protected
void registerDao(Class
entityClass, AbstractDao
dao) { entityToDao.put(entityClass, dao); }
entityToDao是一个DaoSession的一个Map属性,用于保存实体类与Dao的对应关系。
之后会有clear()方法,清空所有的Identity Scope,最后是所有实体类对应的Dao的getter方法。
DAOs
最后是各个实体类对应的Dao classes, 这些DAOs的逻辑较为复杂,通过他们可以实现数据库的增删改查操作了。