greenDAO
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")可以自定义列名。
- @NotNull对应着数据表中该列不能为空。
- @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对象在该线程中的安全性。
- 纯SQL查询
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<User> queryBuilder = userDao.queryBuilder();
queryBuilder.join(Address.class, AddressDao.Properties.userId)
.where(AddressDao.Properties.Street.eq("Sesame Street"));
List<User> 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 <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)
链式连接
/**
* 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)
以下是官方实例:涉及三个实例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<City> 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<Person> 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<Order> 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<Order> 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<Site> 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<? extends AbstractDao<?, ?>> 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<Class<? extends AbstractDao<?, ?>>, 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 <T> void registerDao(Class<T> entityClass, AbstractDao<T, ?> dao) {
entityToDao.put(entityClass, dao);
}
entityToDao是一个DaoSession的一个Map属性,用于保存实体类与Dao的对应关系。
之后会有clear()方法,清空所有的Identity Scope,最后是所有实体类对应的Dao的getter方法。
DAOs
最后是各个实体类对应的Dao classes, 这些DAOs的逻辑较为复杂,通过他们可以实现数据库的增删改查操作了。