GreenDao源码分析及使用GreenDao实现静态数据缓存

一、GreedDao使用方法:

1.

第一步
在project里的dependencies 里添加

classpath'org.greenrobot:greendao-gradle-plugin:3.2.2'

例如

buildscript { repositories { jcenter() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:2.1.0' classpath 'org.greenrobot:greendao-gradle-plugin:3.1.0' } }

2.

第二步 在自己想要用的GreenDao Module 里的 dependencies 里添加

compile'org.greenrobot:greendao:3.2.2'
//compile'org.greenrobot:greendao-generator:3.2.2'//可不加

3.Module

android 里 添加
 greendao {
        schemaVersion 1 //数据库版本号
        daoPackage 'com.example.donny.greendaodemo.greendao.gen' //自动生成的工具类的包名
        targetGenDir 'src/main/java' //路径
    }

    //下面是解释
    schemaVersion: 数据库schema版本,也可以理解为数据库版本号
    daoPackage:设置DaoMaster、DaoSession、Dao包名
    targetGenDir:设置DaoMaster、DaoSession、Dao目录
    targetGenDirTest:设置生成单元测试目录
    generateTests:设置自动生成单元测试用例
头部 添加
apply plugin:'org.greenrobot.greendao'
Activity里初始化
     private void initDb() {
        DaoMaster.DevOpenHelper dbHelper = new DaoMaster.DevOpenHelper(this, "user_db");
        SQLiteDatabase database = dbHelper.getReadableDatabase();
        DaoMaster daoMaster = new DaoMaster(database);
        DaoSession daoSession = daoMaster.newSession();
        mUserDao = daoSession.getUserDao();
      }
查询方法举例
     mTvResult.setText("");
      QueryBuilder<User> builder2 = mUserDao.queryBuilder();
      List<User> list2 = builder2.where(UserDao.Properties.Age.eq(age)).list();
      if (list2.size() > 0) {
          for (int j = 0; j < list2.size(); j++) {
              Long key = mUserDao.getKey(list2.get(j)); //获取id
              Toast.makeText(MainActivity.this, "key:" + key, Toast.LENGTH_SHORT).show();
              mTvResult.setText(mTvResult.getText() + "\n" + list2.get(j).getId() + "," + list2.get(j).getName() + "," + list2.get(j).getAge());
          }
      }
插入可以用save,看源码
       /**
        * "Saves" an entity to the database: depending on the existence of the key property, it will be inserted
        * (key is null) or updated (key is not null).
        * <p>
        * This is similar to {@link #insertOrReplace(Object)}, but may be more efficient, because if a key is present,
        * it does not have to query if that key already exists.
        */
       public void save(T entity) {
           if (hasKey(entity)) {
               update(entity);
           } else {
               insert(entity);
           }
       }

二、源码分析(从使用步骤分析)

new DaoMaster.DevOpenHelper(this, “user_db”);

这是OpenHelper的入口,这里创建了一个表,最终走的是 UserDao.createTable(db, ifNotExists);

看UserDao创建表的方法
       // Creates the underlying database table. 
       public static void createTable(Database db, boolean ifNotExists) {
           String constraint = ifNotExists? "IF NOT EXISTS ": "";
           db.execSQL("CREATE TABLE " + constraint + "\"USER\" (" + //
                   "\"_id\" INTEGER PRIMARY KEY NOT NULL ," + // 0: id
                   "\"NAME\" TEXT," + // 1: name
                   "\"AGE\" INTEGER NOT NULL );"); // 2: age
       }
dbHelper.getReadableDatabase();是获取SqLiteDatabase
DaoMaster daoMaster = new DaoMaster(database);得到daoMaster对象。

看看构造方法

     public DaoMaster(SQLiteDatabase db) {
           this(new StandardDatabase(db));
       }

StandardDatabase是做了一些封装,加了事务等处理。

DaoSession daoSession = daoMaster.newSession();
       public DaoSession newSession() {
           return new DaoSession(db, IdentityScopeType.Session, daoConfigMap);
       }
       public DaoSession(Database db, IdentityScopeType type, Map<Class<? extends AbstractDao<?, ?>>, DaoConfig>
               daoConfigMap) {
           super(db);
           userDaoConfig = daoConfigMap.get(UserDao.class).clone();
           userDaoConfig.initIdentityScope(type);
           userDao = new UserDao(userDaoConfig, this);
           registerDao(User.class, userDao);
       }
mUserDao = daoSession.getUserDao();
     public UserDao getUserDao() {
           return userDao;
       }
QueryBuilder builder = mUserDao.queryBuilder();
   public QueryBuilder<T> queryBuilder() {
       return QueryBuilder.internalCreate(this);
   }
List list = builder.where(UserDao.Properties.Id.eq(id)).list();
where语句
   /**
    * Adds the given conditions to the where clause using an logical AND. To create new conditions, use the properties
    * given in the generated dao classes.
    */
   public QueryBuilder<T> where(WhereCondition cond, WhereCondition... condMore) {
       whereCollector.add(cond, condMore);
       return this;
   }
   //WhereCollector是一个类,用来存储where语句的集合。
list方法中的build和list方法
       public List<T> list() {
           return build().list();
       }
   看一下build里面做了写什么
      /**
        * Builds a reusable query object (Query objects can be executed more efficiently than creating a QueryBuilder for
        * each execution.
        */
       public Query<T> build() {
           StringBuilder builder = createSelectBuilder();
           int limitPosition = checkAddLimit(builder);
           int offsetPosition = checkAddOffset(builder);
           String sql = builder.toString();
           checkLog(sql);
           return Query.create(dao, sql, values.toArray(), limitPosition, offsetPosition);
       }
再看QueryBuilder里面的create
      static <T2> Query<T2> create(AbstractDao<T2, ?> dao, String sql, Object[] initialValues, int limitPosition,
                                    int offsetPosition) {
           QueryData<T2> queryData = new QueryData<T2>(dao, sql, toStringArray(initialValues), limitPosition,
                   offsetPosition);
           return queryData.forCurrentThread();
       }
Query里面的list()方法
     public List<T> list() {
           checkThread();
           Cursor cursor = dao.getDatabase().rawQuery(sql, parameters);
           return daoAccess.loadAllAndCloseCursor(cursor);
       }

到现在为止,我们知道List list这个集合怎么来的了。

mUserDao.insert(new User(id, name, age));
   /**
    * Insert an entity into the table associated with a concrete DAO.
    *
    * @return row ID of newly inserted entity
    */
   public long insert(T entity) {
       return executeInsert(entity, statements.getInsertStatement(), true);
   }
先看TableStatements里的getInsertStatement方法
      public DatabaseStatement getInsertStatement() {
               if (insertStatement == null) {
                   String sql = SqlUtils.createSqlInsert("INSERT INTO ", tablename, allColumns);
                   DatabaseStatement newInsertStatement = db.compileStatement(sql);
                   synchronized (this) {
                       if (insertStatement == null) {
                           insertStatement = newInsertStatement;
                       }
                   }
                   if (insertStatement != newInsertStatement) {
                       newInsertStatement.close();
                   }
               }
               return insertStatement;
           }
SqlUtils里面有各种方法拼凑Sql语句
    public static String createSqlInsert(String insertInto, String tablename, String[] columns) {
           StringBuilder builder = new StringBuilder(insertInto);
           builder.append('"').append(tablename).append('"').append(" (");
           appendColumns(builder, columns);
           builder.append(") VALUES (");
           appendPlaceholders(builder, columns.length);
           builder.append(')');
           return builder.toString();
       }

最终得到的语句是 INSERT INTO “USER” (“_id”,”NAME”,”AGE”) VALUES (?,?,?)

DatabaseStatement newInsertStatement = db.compileStatement(sql);执行sql语句

我们注意到,是有加同步的。

executeInsert(这里好多地方都命名有Tx,带Tx的意思是开启事务了的)
  private long executeInsert(T entity, DatabaseStatement stmt, boolean setKeyAndAttach) {
           long rowId;
           if (db.isDbLockedByCurrentThread()) {
               rowId = insertInsideTx(entity, stmt);
           } else {
               // Do TX to acquire a connection before locking the stmt to avoid deadlocks
               db.beginTransaction();
               try {
                   rowId = insertInsideTx(entity, stmt);
                   db.setTransactionSuccessful();
               } finally {
                   db.endTransaction();
               }
           }
           if (setKeyAndAttach) {
               updateKeyAfterInsertAndAttach(entity, rowId, true);
           }
           return rowId;
       }
insertInsideTx
    private long insertInsideTx(T entity, DatabaseStatement stmt) {
           synchronized (stmt) {
               if (isStandardSQLite) {
                   SQLiteStatement rawStmt = (SQLiteStatement) stmt.getRawStatement();
                   bindValues(rawStmt, entity);
                   return rawStmt.executeInsert();
               } else {
                   bindValues(stmt, entity);
                   return stmt.executeInsert();
               }
           }
       }

这个方法是最终要执行的方法,看看下面的子方法解析就知道了

看看UserDao里面的bindValurs
     @Override
       protected final void bindValues(DatabaseStatement stmt, User entity) {
           stmt.clearBindings();
           stmt.bindLong(1, entity.getId());
           String name = entity.getName();
           if (name != null) {
               stmt.bindString(2, name);
           }
           stmt.bindLong(3, entity.getAge());
       }
StandardDatabaseStatement里的executeInsert最终走的是原生的executeInsert方法
    private final SQLiteStatement delegate;
       @Override
       public long executeInsert() {
           return delegate.executeInsert();
       }

至于SQLiteStatement是怎么回事,可以看下面链接。
对于执行纯sql,ContentValues和compileStatement最终都是new 一个SQLiteStatement对象,并调用SQLiteStatement对象的相应方法。
http://blog.csdn.net/wangbole/article/details/43196067

三、源码进一步分析,介绍自动生成的几个关键类和层级关系

首先是AbstractDaoMaster ,它是抽象类,功能是在子类实现的。看DaoMaster,它是子类,主要方法有

1)创建表,UserDao里的createTable创建,上面有贴UserDao类代码。升级就是drop原先表,create新表。
    public DaoMaster(SQLiteDatabase db) {
           this(new StandardDatabase(db));
       }

       public DaoMaster(Database db) {
           super(db, SCHEMA_VERSION);
           registerDaoClass(UserDao.class);
       }
        protected void registerDaoClass(Class<? extends AbstractDao<?, ?>> daoClass) {
            DaoConfig daoConfig = new DaoConfig(db, daoClass);
            daoConfigMap.put(daoClass, daoConfig);
        }

       // Creates underlying database table using DAOs. 
        public static void createAllTables(Database db, boolean ifNotExists) {
            UserDao.createTable(db, ifNotExists);
        }
         /** Drops underlying database table using DAOs. */
            public static void dropAllTables(Database db, boolean ifExists) {
                UserDao.dropTable(db, ifExists);
            }
2)可以看到DevOpenHelper是主要的创建和升级的类。DevOpenHelper 和 EncryptedDevOpenHelper 会在升级的时候删除所有表并重建,前者是用来操作未加密数据库,后者是用来操作加密数据库;
     /**
         * 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);
           }
       }
3)这里可以看到层级关系,Master->Session
   public DaoSession newSession() {
       return new DaoSession(db, IdentityScopeType.Session, daoConfigMap);
   }

   public DaoSession newSession(IdentityScopeType type) {
       return new DaoSession(db, type, daoConfigMap);
   }

session 缓存类。DaoSession构造里有new UserDao,所以可以看到层级关系DaoSession->UserDao。

一进来就走了registerDao()

     public DaoSession(Database db, IdentityScopeType type, Map<Class<? extends AbstractDao<?, ?>>, DaoConfig>
               daoConfigMap) {
           super(db);

           userDaoConfig = daoConfigMap.get(UserDao.class).clone();
           userDaoConfig.initIdentityScope(type);

           userDao = new UserDao(userDaoConfig, this);

           registerDao(User.class, userDao);
       }

而registerDao()里面是存放dao数据到map集合。拿自然对应的get了

     protected <T> void registerDao(Class<T> entityClass, AbstractDao<T, ?> dao) {
           entityToDao.put(entityClass, dao);
       }

这里有增删改查快捷调用,实际上还是调用的UserDao的方法,如下

     /** Convenient call for {@link AbstractDao#insert(Object)}. */
         public <T> long insert(T entity) {
          @SuppressWarnings("unchecked")
          AbstractDao<T, ?> dao = (AbstractDao<T, ?>) getDao(entity.getClass());
          return dao.insert(entity);
        }

AbstractDao是所有Dao的基类,实现各个bean的增删改查操作,那么就有了层级关系UserDao->UserEntity

通过 class UserDao extends AbstractDao

load方法

这里的rawQuery最终调用的是SqliteDatabase里的rawQuery

    /**
           * Loads the entity for the given PK.
           *
           * @param key a PK value or null
           * @return The entity or null, if no entity matched the PK value
           */
          public T load(K key) {
              assertSinglePk();
              if (key == null) {
                  return null;
              }
              if (identityScope != null) {
                  T entity = identityScope.get(key);
                  if (entity != null) {
                      return entity;
                  }
              }
              String sql = statements.getSelectByKey();
              String[] keyArray = new String[]{key.toString()};
              Cursor cursor = db.rawQuery(sql, keyArray);
              return loadUniqueAndCloseCursor(cursor);
          }

还是继续看load是如何拿到数据的

    /** Internal use only. Considers identity scope. */
      final protected T loadCurrent(Cursor cursor, int offset, boolean lock) {
          if (identityScopeLong != null) {
              if (offset != 0) {
                  // Occurs with deep loads (left outer joins)
                  if (cursor.isNull(pkOrdinal + offset)) {
                      return null;
                  }
              }
              long key = cursor.getLong(pkOrdinal + offset);
              T entity = lock ? identityScopeLong.get2(key) : identityScopeLong.get2NoLock(key);
              if (entity != null) {
                  return entity;
              } else {
                  entity = readEntity(cursor, offset);
                  attachEntity(entity);
                  if (lock) {
                      identityScopeLong.put2(key, entity);
                  } else {
                      identityScopeLong.put2NoLock(key, entity);
                  }
                  return entity;
              }
          } else if (identityScope != null) {
              K key = readKey(cursor, offset);
              if (offset != 0 && key == null) {
                  // Occurs with deep loads (left outer joins)
                  return null;
              }
              T entity = lock ? identityScope.get(key) : identityScope.getNoLock(key);
              if (entity != null) {
                  return entity;
              } else {
                  entity = readEntity(cursor, offset);
                  attachEntity(key, entity, lock);
                  return entity;
              }
          } else {
              // Check offset, assume a value !=0 indicating a potential outer join, so check PK
              if (offset != 0) {
                  K key = readKey(cursor, offset);
                  if (key == null) {
                      // Occurs with deep loads (left outer joins)
                      return null;
                  }
              }
              T entity = readEntity(cursor, offset);
              attachEntity(entity);
              return entity;
          }
      }

上面的readEntity是在子类实现的

      @Override
      public User readEntity(Cursor cursor, int offset) {
          User entity = new User( //
              cursor.getLong(offset + 0), // id
              cursor.isNull(offset + 1) ? null : cursor.getString(offset + 1), // name
              cursor.getInt(offset + 2) // age
          );
          return entity;
      }

再大致看一下update的核心方法吧,bindValue()是在AbstractDao的子类完成的,主要调了SqliteStatement的bind方法和excute方法

      protected void updateInsideSynchronized(T entity, SQLiteStatement stmt, boolean lock) {
          // To do? Check if it's worth not to bind PKs here (performance).
          bindValues(stmt, entity);
          int index = config.allColumns.length + 1;
          K key = getKey(entity);
          if (key instanceof Long) {
              stmt.bindLong(index, (Long) key);
          } else if (key == null) {
              throw new DaoException("Cannot update entity without key - was it inserted before?");
          } else {
              stmt.bindString(index, key.toString());
          }
          stmt.execute();
          attachEntity(key, entity, lock);
      }

由于篇幅原因不一一列举了,这里涉及到增删改查方法的,我都只列举了一个方法来举例,其它几个原理都类似

四、补充:其它使用方式,常用方法

查询

QueryBuilder builder = mUserDao.queryBuilder();
Query query = builder.build();
User unique = query.unique();#**清除缓 daoSession.clear();

    projectDao = daoSession.getNoteDao();
    projectDao.detachAll();

注解的使用

@Entity @Id
 @Entity
    public class Customer { //@Entity表示它是一个Entity,会生成对应的UserDao,也可以@Entity(generateConstructors = false)  
      @Id
      private Long id;
    }
    @Entity
    public class Order {
      @Id
      private Long id; //@Id表示它是id主键,@Id(autoincrement = true)设置自增长
      private Date date;
      @Property(nameInDb = "CUSTOMER_ID")
      private long customerId;
    }
@Property

让你自定义字段在数据库中的名称,如果为空,GreenDAO将根据驼峰法将其用”_”分割,并全部转为大写,如userName 变为 USER_NAME

@Generated

自动生成的代码会有此注解,如果你加了此注解,它就不会自动生成代码了

@ToOne

直接拿网上的例子了。
Customer表通过id与Order表关联,查询Order的Customer时需要先知道Order中的customerId然后根据id = customerId值再去数据库中查询该id所对应的Customer对象。
然而在greenDao中一个注释就可以搞定,只需要使用
@ToOne注释来定义一个关联对象即可。
@ToOne 定义了一个entities与另一个entities的1:1 对应关系。通过joinProperty参数来定义一个外键下面是代码示例

    public class Order {
        @Id
        private Long id;
        private long customerId;
        @ToOne(joinProperty = "customerId")
        private Customer customer;
    }
    @Entity
    public class Customer {
        @Id
        private Long id;
    }

这样只要获得Order对象就能通过getCustomer() 方法获取Order所对应的Customer了。

@ToMany

@ToMany 定义和多个实体之间的关系。此注解适用于其他实体对象集合的字段。有三种方式可以用来实现多映射。
referencedJoinProperty:指定目标实体中与源实体相对应的外键。

    @Entity
    public class User {
        @Id private Long id;

        @ToMany(referencedJoinProperty = "ownerId")
        private List<Site> ownedSites;
    }

    @Entity
    public class Site {
        @Id private Long id;
        private long ownerId;
    }
joinProperty: 对于复杂一点的关系可以定义一组@JoinProperty注解。每个@JoinProperty注解都需要有源实体中的源属性和对应实体中的引用属性。
  @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;
    }

五、常见问题:

1.关于自动bean类代码的生成

bean类很多代码是会在项目运行以后自动生成的,比如@ToOne注解的代码、构造方法。可以先写bean类,写好后运行,
等它自动生成相关代码以后再写其他代码。当然了,写了bean要运行以后才会生成Dao、Session等文件。
如果你需要再次修改bean类的变量,那就需要修改对应的自动生成的方法,怎么改?删除这些方法再运行就可以了。

2.关于主键

一般后台给我们返回的id是int型的,而且不一定是唯一的,肯定不能作为主键,所以我们需要自己定义一个主键。
比如 private Long _id; 注意这个数据类型是Long,而不是long,否则会报错,这个问题坑了我好久。

3.怎么关联ModelBean和Versionbean?

我们后台返回的数据结构是这样的,已banner为例
 {
      "code": xxx,
      "msg": "请求成功",
      "data": [
          {
              "id": 100,
              "img": "http://xxx.jpg",
              "link": "",
              "type": 1,
              "isShow": 1,
              "createTime": 1496401858000,
              "jumpType": 1,
              "jumpId": 0
          },
          {
              "id": 101,
              "img": "http://xxx.jpg",
              "link": "",
              "type": 1,
              "isShow": 1,
              "createTime": 1496402018000,
              "jumpType": 1,
              "jumpId": 0
          },

      ],
      "version": "1.0.0"
    }
需要把VersionBean和ModelBean关联起来,那么可以在网络请求成功以后这样做,responseList.version就是后台返回的数据
    CacheVersion cacheVersion = new CacheVersion();
    cacheVersion.setVersion(responseList.version);
    mCacheVersionDao.save(cacheVersion);

4.同一个bean好几个页面都用到,怎么处理?

其实很简单,在这个bean里加一个字段就可以了。比如我加了 private String category; 然后存储的时候在每个bean里
设置一个值给它就行了。比如: 我在IConstant里这样写的,然后设置这个值为category就可以了。
    class CATEGORY {
          public static final String BANNER_CONVERT = "banner_convert";
          public static final String BANNER_KECHIEN = "banner_kechien";
          public static final String BANNER_VOLUNTEER = "banner_volunteer";
          public static final String IMAGE_CONVERT = "image_convert";
          public static final String IMAGE_KECHIEN = "image_kechien";
          public static final String IMAGE_VOLUNTEER = "image_volunteer";
          public static final String CODE_BY_TYPE_SHARE = "code_share";
      }

5.建表的任务可以放在Application里面做。

6.我是在activity先初始化缓存数据,如果有数据就刷新界面。然后进行网络请求,如果网络请求返回304,就不处理。如果返回200,就存储数据到数据库,并更新界面。

初始化缓存数据代码,因为有category的区分,不能直接loadAll。

    private void initBannerData() {
        QueryBuilder<Banner> bannerQueryBuilder = mBannerDao.queryBuilder();
        List<Banner> banners = bannerQueryBuilder.where(BannerDao.Properties.Category.eq(IConstants.CATEGORY.BANNER_CONVERT)).build().list();
        if (banners == null || banners.isEmpty()) {
            return;
        }
        adapter.addAll(banners);
        adapter.notifyItemChanged(0, "banner");

网络请求成功里面的代码

                if (response.code == 12000) {
                    List<Banner> banners = response.data;
                    if (cacheVersionDao != null && bannerDao != null) {
                        CacheVersion cacheVersion = new CacheVersion();
                        cacheVersion.setVersion(response.version);
                        cacheVersionDao.save(cacheVersion);
                        for (Banner banner :
                            banners) {
                            banner.setCategory(category);
                            banner.setCacheVersion(cacheVersion);
                        }
                        bannerDao.saveInTx(banners);
                    }
                    callback.onSuccess(banners);//回调出去
                }

7.我写的demo不够完善,完整的项目又不能共享出来,所以就不放demo了,懒得写demo…

发布了40 篇原创文章 · 获赞 6 · 访问量 2万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览