preface

Native database direct operation is not easy to use?

Why use GreenDao?

What is GreenDao’s advantage?

This paper will analyze the usage layer of GreenDao and the principle of generating static code without GreenDao template.

directory

A, SQLite

SQLite is a lightweight, cross-platform relational database.

To use the native database SQLite, you need to use the auxiliary class SQLiteOpenHelper provided by Android, which can be inherited from it to achieve the creation of the database, upgrade, update the database when some operations, etc. The prerequisite is that you have some understanding of SQL statements

Advantages:

  • Lightweight and small in size

  • Do not need to install

  • Cross-platform/portable

  • The database information is in a file

Disadvantages:

  • In concurrent scenarios, the database may be monopolized by write operations, causing other read and write operations to block or fail

  • SQL standard support is incomplete

Precautions for use:

  • Sqlite writes are not thread-safe

  • Sqlite reads are thread-safe

  • When inserting a large amount of data, it is best to enable transactions to avoid single circular inserts and multiple reads and writes to the disk. Sqlite defaults to one transaction for each insert statement

  • Using the execSQL method can lead to injection attacks

For SQL statement execution, you can use the execSQL() method of SQLiteDatabase, or you can use ContentValues to insert and modify data, etc.

When querying, we might need to write something like the following code

List<Person> result = new ArrayList<>(); Cursor cursor = null; SQLiteDatabase readableDatabase = mDbHelper.getReadableDatabase(); try { cursor = readableDatabase.rawQuery("select * from person where age > ? order by age desc", new String[]{"18"}); while (cursor.moveToNext()) { String id = cursor.getString(cursor.getColumnIndex("_id")); String name = cursor.getString(cursor.getColumnIndex("NAME")); String sex = cursor.getString(cursor.getColumnIndex("SEX")); String age = cursor.getString(cursor.getColumnIndex("AGE")); Person person = new Person(id,name,sex,age); result.add(person); } } catch (Exception e) { e.printStackTrace(); } finally {// Close cursor if (cursor! = null) { cursor.close(); cursor = null; } // Close database readabledatabase.close (); }Copy the code

When we parse the data, we need to manipulate the cursor and then iterate through the match, build the object, return the data set, which is not very friendly for application layer developers.

Second, the GreenDao

GreenDao is an object-relational mapping framework that provides an interface to manipulate relational databases by manipulating objects.

GreenDao website

1. Advantages and disadvantages

Advantages:

  • High performance, the fastest Android relational database, fast access

  • Dependent on small volume

  • Low memory overhead

  • SQLCipher encryption is supported

  • Strong community support

Disadvantages:

  • You need to get the Dao class, modify the entity, and Rebuild

  • The default database upgrade will delete all tables and create a new one. In this case, pay attention to the adaptation of the old APP and check whether the database type has changed

2. Easy to use

  • The import

    The main engineering build. Gradle

    Classpath 'org. Greenrobot: greendao - gradle - plugin: 3.3.0'Copy the code

    Module build. Gradle

    Apply the plugin: 'org. Greenrobot. Greendao' dependencies {API 'org. Greenrobot: greendao: 3.2.0'}Copy the code
  • Writing entity classes

    Custom.class

    @Entity
    public class Custom {
        @Id(autoincrement = true)
        private Long id;
    ​
        private String shopName;
    ​
        private String shopDescription;
      }
    Copy the code

    Rebuild or synchronize the Custom class as follows

    @Entity public class Custom { @Id(autoincrement = true) private Long id; private String shopName; private String shopDescription; @Generated(hash = 567200405) public Custom(Long id, String shopName, String shopDescription) { this.id = id; this.shopName = shopName; this.shopDescription = shopDescription; } @Generated(hash = 62298964) public Custom() { } public Long getId() { return this.id; } public void setId(Long id) { this.id = id; } public String getShopName() { return this.shopName; } public void setShopName(String shopName) { this.shopName = shopName; } public String getShopDescription() { return this.shopDescription; } public void setShopDescription(String shopDescription) { this.shopDescription = shopDescription; }}Copy the code

    The corresponding directory generates such files

  • Get and use the Dao class

    Example add, delete, update, find

    DBManager.class

    private DBManager(){
            if(openHelper == null) {
                openHelper = new DaoMaster.DevOpenHelper(Global.getInstance().getApplicationContext(), "custom_db");
                SQLiteDatabase sqLiteDatabase = openHelper.getWritableDatabase();
                daoSession = new DaoMaster(sqLiteDatabase).newSession();
            }
        }
        public DaoSession getDaoSession(){
            return  daoSession;
        }
    Copy the code

    DBOperationInterface

    public interface DBOperationInterface<T> {
        void insert(List<T> data);
        void delete(List<T> data);
        void update(List<T> data);
        List<T> find(Object... args);
    }
    Copy the code

    CustomOperation

    Public class CustomOperation extends BaseOperation<Custom> {public void insert(List<Custom> data) {if (data == null || data.isEmpty()) { return; } getDaoSession().getCustomDao().saveInTx(data); } / / delete @ Override public void the delete (List < Custom > data) {if (data = = null | | data. The isEmpty () {return; } getDaoSession().getCustomDao().deleteInTx(data); } / / @ Override update public void update (List < Custom > data) {if (data = = null | | data. The isEmpty () {return; } getDaoSession().getCustomDao().updateInTx(data); } @override public List<Custom> find(Object... args) { if (args == null || args.length == 0) { return getDaoSession().getCustomDao().loadAll(); } QueryBuilder<Custom> queryBuilder = getDaoSession().getCustomDao().queryBuilder(); if (args.length == 1 && args[0] instanceof String) { return queryBuilder.where(CustomDao.Properties.ShopName.eq(args[0])).list(); } else if (args.length == 2 ) { if (args[0] instanceof String && args[1] instanceof String) { return queryBuilder.where(CustomDao.Properties.ShopName.eq(args[0]), CustomDao.Properties.ShopDescription.eq(args[1])).list(); } } return null; }}Copy the code

3. Principle analysis

The principle of using GreenDao will be analyzed below. Since insertion is similar to update and delete, only FE is inserted here

3.1 DevOpenHelper creation

The class diagram below

/** 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); }}Copy the code

The constructor calls the constructor of the parent OpenHelper class. During onUpgrade, all tables will be deleted first and then the parent onCreate class will be called to create a new table. If you do not want to do this, you can customize a class that inherits OpenHelper to replace the official default DevOpenHelper.

OpenHelper is derived from DatabaseOpenHelper. In this class, it is important to note that two different types of databases are internally represented: StandardDatabase and EncryptedDatabase. The former is not encrypted. The latter uses SQLCipher encryption

 public Database getWritableDb() {
     return wrap(getWritableDatabase());
 }
 public Database getEncryptedWritableDb(String password) {
     EncryptedHelper encryptedHelper = checkEncryptedHelper();
     return encryptedHelper.wrap(encryptedHelper.getReadableDatabase(password));
 }
Copy the code

3.2 Creation of DaoSession

The related process is shown below

DaoSession creates a DaoMaster, passing DataBase or SQLiteDatabase. If SQLiteDataase is passed, it will be wrapped in the constructor. Use StandardDatabase to broker SQLiteDatabase

public DaoMaster(SQLiteDatabase db) {
    this(new StandardDatabase(db));
}
public DaoMaster(Database db) {
     super(db, SCHEMA_VERSION);
     registerDaoClass(CustomDao.class);
 }
Copy the code

In the DaoMaster constructor, you can also initialize HashMap daoConfigMap by calling the AbstractDaoMaster constructor via super.

Build the DaoConfig object via the following registerDaoClass(CustomDAO.class) (where you create a TableStatements class related to SQL statement execution). DaoClass and DaoConfig mappings are also stored.

After constructing the DaoMaster class, return the DaoSeesion class by calling its **newSession** method

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

What does DaoSession do in its constructor?

public DaoSession(Database db, IdentityScopeType type, Map<Class<? extends AbstractDao<? ,? >>, DaoConfig> daoConfigMap) { super(db); customDaoConfig = daoConfigMap.get(CustomDao.class).clone(); customDaoConfig.initIdentityScope(type); customDao = new CustomDao(customDaoConfig, this); registerDao(Custom.class, customDao); } protected <T> void registerDao(Class<T> entityClass, AbstractDao<T, ? > dao) { entityToDao.put(entityClass, dao); }Copy the code

Instantiate the entityToDao, generate the customDaoConfig object, and store the custom. class mapping to the CustomDao into the entityToDao. CustomDaoConfig also initializes IdentityScope

public void initIdentityScope(IdentityScopeType type) { if (type == IdentityScopeType.None) { identityScope = null; } else if (type == IdentityScopeType.Session) { if (keyIsNumeric) { identityScope = new IdentityScopeLong(); } else { identityScope = new IdentityScopeObject(); } } else { throw new IllegalArgumentException("Unsupported type: " + type); }}Copy the code

This IdentityScope is used to hold the cache of mappings between ID and entity classes.

3.3, insert,

Schematic diagram

Analysis of the

public void saveInTx(Iterable<T> entities) { int updateCount = 0; int insertCount = 0; For (T entity: entities) {if (hasKey(entity)) {updateCount++; } else { insertCount++; }} if (updateCount > 0 &&insertCount > 0) {List<T> toUpdate = new ArrayList<>(updateCount); List<T> toInsert = new ArrayList<>(insertCount); for (T entity : entities) { if (hasKey(entity)) { toUpdate.add(entity); } else { toInsert.add(entity); } } db.beginTransaction(); try { updateInTx(toUpdate); insertInTx(toInsert); db.setTransactionSuccessful(); } finally { db.endTransaction(); }} else if (insertCount > 0) {entities; } else if (updateCount > 0) { updateInTx(entities); }}Copy the code

If there is an ID, hasKey returns true, and then the transaction is started to update and insert data. If there is an ID, hasKey returns true. But calling saveInTx as an insert usually goes directly to comment 3 and calls insertInTx

public void insertInTx(Iterable<T> entities) { insertInTx(entities, isEntityUpdateable()); Public void insertInTx(Iterable<T> entities, Iterable<T> entities) boolean setPrimaryKey) { DatabaseStatement stmt = statements.getInsertStatement(); executeInsertInTx(stmt, entities, setPrimaryKey); }Copy the code

The key is the executeInsertInTx method

private void executeInsertInTx(DatabaseStatement stmt, Iterable<T> entities, boolean setPrimaryKey) { db.beginTransaction(); try { synchronized (stmt) { if (identityScope ! = null) { identityScope.lock(); } try {if (isStandardSQLite) {SQLiteStatement rawStmt = (SQLiteStatement) STMT. GetRawStatement (); For (T entity: entities) {// annotation 2 bindValues(rawStmt, entity); If (setPrimaryKey) {// comment 3 Long rowId = rawstmt.executeInsert (); / / comment 4 updateKeyAfterInsertAndAttach (entity, the rowId, false); } else { rawStmt.execute(); } } } else { ...... } } finally { if (identityScope ! = null) { identityScope.unlock(); } } } db.setTransactionSuccessful(); } finally { db.endTransaction(); }}Copy the code

We are using an unencrypted database, so isStandardSQLite is true, comment 1 is to get the SQLiteStatement object in the Android API from STMT, then comment 2, Set the parameter value for SQLiteStatement, insert statement at comment 3 to get the ID value, then set the ID for the entity entity class at Comment 4 and call attachEntity to store the ID and entity class relational mapping in IdentityScope. The bindValues and attachEntity methods are as follows

protected final void bindValues(SQLiteStatement stmt, Custom entity) { stmt.clearBindings(); Long id = entity.getId(); if (id ! = null) { stmt.bindLong(1, id); } String shopName = entity.getShopName(); if (shopName ! = null) { stmt.bindString(2, shopName); } String shopDescription = entity.getShopDescription(); if (shopDescription ! = null) { stmt.bindString(3, shopDescription); }}Copy the code

3.4, find

Schematic diagram

Analysis of the

Let’s look at the loadAll method here

public List<T> loadAll() { Cursor cursor = db.rawQuery(statements.getSelectAll(), null); return loadAllAndCloseCursor(cursor); } protected List<T> loadAllAndCloseCursor(Cursor cursor) { try { return loadAllFromCursor(cursor); } finally { cursor.close(); }}Copy the code

The loadAllAndCloseCursor method is ultimately called

protected List<T> loadAllFromCursor(Cursor cursor) { int count = cursor.getCount(); if (count == 0) { return new ArrayList<T>(); } List<T> list = new ArrayList<T>(count); CursorWindow window = null; boolean useFastCursor = false; If (cursor instanceof CrossProcessCursor) {window = ((CrossProcessCursor) cursor).getwindow (); if (window ! = null) { // E.g. Robolectric has no Window at this point if (window.getNumRows() == count) { cursor = new FastCursor(window); useFastCursor = true; } else { DaoLog.d("Window vs. result size: " + window.getNumRows() + "/" + count); } } } if (cursor.moveToFirst()) { if (identityScope ! = null) { identityScope.lock(); identityScope.reserveRoom(count); } try {// comment 2 if (! useFastCursor && window ! = null && identityScope ! = null) { loadAllUnlockOnWindowBounds(cursor, window, list); } else {// comment 3 do {list.add(loadCurrent(cursor, 0, false)); } while (cursor.moveToNext()); } } finally { if (identityScope ! = null) { identityScope.unlock(); } } } return list; }Copy the code

Comment 1 determines whether the Cursor is cross-process, and if numRows == count is correct, FastCurosr is created for traversal, and comment 3 is used, otherwise comment 2 is used. The loadCurrent method is used to collect data in note 3, as well as in note 2

private void loadAllUnlockOnWindowBounds(Cursor cursor, CursorWindow window, List<T> list) { int windowEnd = window.getStartPosition() + window.getNumRows(); for (int row = 0; ; row++) { list.add(loadCurrent(cursor, 0, false)); row++; . if (! cursor.moveToNext()) { break; . }}Copy the code

So we can analyze the loadCurrent method directly

Final protected T loadCurrent(Cursor Cursor, int offset, Boolean lock) {// note 1 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); // comment 2 T entity = lock? identityScopeLong.get2(key) : identityScopeLong.get2NoLock(key); if (entity ! = null) { return entity; } else {// annotation 3 entity = readEntity(cursor, offset); attachEntity(entity); if (lock) { identityScopeLong.put2(key, entity); } else { identityScopeLong.put2NoLock(key, entity); } return entity; }}... }Copy the code

Since the ID in the entity class Custom we created is of type long, identityScopeLong is not empty in comment 1.

Remember when we analyzed insert data earlier, we mentioned that ID and entity-class relational mappings were stored in IdentityScope? If not, go to Comment 3 and call readEntity to read the data from IdentityScope

    @Override
    public Custom readEntity(Cursor cursor, int offset) {
        Custom entity = new Custom( //
            cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0), // id
            cursor.isNull(offset + 1) ? null : cursor.getString(offset + 1), // shopName
            cursor.isNull(offset + 2) ? null : cursor.getString(offset + 2) // shopDescription
        );
        return entity;
    }
Copy the code

Third, summary

SQLite is a relational database, in the code to use their own manual implementation of the creation of cursor query, delete and other operations code, the operation is very cumbersome.

GreenDao, as an object-relational mapping framework, encapsulates most of the code we need to implement ourselves

  • The proxy mode is used to encapsulate different SQL statements

  • It also has a caching function for lookups,

  • SQLCipher encryption and non-encryption two ways are provided, and it is convenient to use

  • The insertion time is about the same, but the deletion and update of tens of thousands of pieces of data is almost 1 to 40

If there is any mistake, please point out, thank you!