Android(安卓)GreenDao使用总结(包括模型生成、增删改查、修改存储路径、数据库更新升级和加解密数据库)
16lz
2021-01-26
目录:
前言: 在Android开发中,或多或少总要接触SQLite。然而在使用它时,我们往往需要做许多额外的工作,像编写 SQL 语句与解析查询结果等。所以,适用于 Android 的ORM 框架也就孕育而生了,现在市面上主流的框架有 OrmLite、SugarORM、Active Android、Realm 与 GreenDAO。而greenDAO号称是速度最快的ORM(见官网)。 简单的讲,greenDAO 是一个将对象映射到 SQLite 数据库中的轻量且快速的 ORM 解决方案。
下面,我将详解地介绍如何在 Android Studio 上使用 greenDAO,并结合代码总结一些使用过程中的心得。
导入依赖和Schema设置: 导入依赖和Schema参数设置详细内容可参见官网和如下网址: http://www.jianshu.com/p/4e6d72e7f57a 我的Schema设置如下:
(1)编写greendaoGenerator的纯Java类库,以生成DaoMaster、DaoSession、bean和beanDao等; 可参见: http://www.open-open.com/lib/view/open1438065400878.html 注:greenDao 3.0版本以下只能采用这种方式。在升级3.0以后,因支持了注释方式,该方式不再推荐。 (2)利用注释的方式,生成 DaoMaster、DaoSession、bean和beanDao等; 2、下文仅就注释方式生成流程进行简单的介绍,详细的讲解可参见 http://www.jianshu.com/p/4e6d72e7f57a 首先,新建datamodel包,用以包含 DaoMaster、DaoSession、bean和beanDao等。 然后新建Area实体类,代码如下:
二、修改数据库文件路径: 默认情况下,新创建的数据存储在data的包名目录下,设备如果不root的话,是无法查看SQLite数据库文件的。而实际应用中,我们往往需要copy数据库,或借用第三方工具查阅或编辑数据库内容。此时我们可以通过重写Context的 getDatabasePath( String name ) 、 openOrCreateDatabase(String name, int mode, CursorFactory factory) 、 openOrCreateDatabase(String name, int mode, CursorFactory factory, DatabaseErrorHandler errorHandler) 等三个方法 来修改SQLite文件的存储路径。 通过查询资料,发现 http://blog.csdn.net/chenzhenlindx/article/details/39183691 中的内容基本符合我们的需求。但是博主是在DaoMaster中重写方法的。通过上文我们知道,DaoMaster的代码是不能修改的。因此,我们可以将重写的方法放到GreenDaoHelper中去。 代码如下:
三、获取加密的数据库: 修改GreenDaoHelper.java,通过调用DaoMaster.OpenHelper类的getEncryptedWritableDb(password)或者 getEncryptedReadableDb(password)方法, 即可获取加密的数据库。
四、数据库升级又不删除数据: 在实际开发的过程中,数据库的结构可能会有所改变。而 使用 DevOpenHelper 每次升级数据库时,表都会删除重建。因此,实际使用中需要建立类继承 DaoMaster.OpenHelper,实现 onUpgrade()方法。通过查询资料,对未加密的数据库,推荐使用 升级辅助库 GreenDaoUpgradeHelper( 可参见 https://github.com/yuweiguocn/GreenDaoUpgradeHelper/blob/master/README_CH.md )。该库通过 MigrationHelper在删表重建的过程中,使用临时表保存数据并还原。 示例程序 直接导入 MigrationHelper.java 源码。同时修改GreenDaoHelper.java文件,新建一个继承自 DaoMaster.OpenHelper的内部类MySQLiteOpenHelper。具体代码如下:
EncryptedHelper内部类的 onUpgrade()方法调用的是 DatabaseOpenHelper抽象类本身的 onUpgrade()方法,但DatabaseOpenHelper抽象类本身的 onUpgrade()方法默认是啥也不执行的,如下图所示。因此, MigrationHelper.migrate() 为什么不支持 对 加密的 数据库的更新也就显而易见了。
(2)对加密的数据库的更新支持的解决方案 此时,可以通过修改greenDao的源码,在 EncryptedHelper内部类的 onUpgrade()方法中添加对 MigrationHelper.migrate()的调用。 但,如果我不想修改源码,该怎么解决呢?默认情况下,获取和更新加密的数据库,调用的是 DatabaseOpenHelper抽象类本身的getEncryptedWritableDb(String password)和 onUpgrade()方法 。此时,我们可以在GreenDaoHelper中定义一个新的类MyEncryptedSQLiteOpenHelper继承自DaoMaster.OpenHelper,并在这个类中对这两个方法进行重写,同时在内部自定义一个net.sqlcipher.database.SQLiteOpenHelper的继承类,以代替 DatabaseOpenHelper抽象类里的EncryptedHelper内部类。 在添加代码之前,要先添加对sqlcipher的依赖,如下:
至此,我们已能够对加密的数据库进行更新支持。
五、总结: 该博客对greenDao模型生成、增删改查、存储路径 修改 、加解密及更新升级等都有了较为详细的描述,在实际开发中也基本够用。不过,需要注意的是,本文引用的EncryptedMigrationHelper.java类和 MigrationHelper.java类均只支持对数据库对象的新增和删除,对于对象字段有修改的情况没做考虑。如果读者有需要的话,可另行开发。
参考文献如下: 官网: http://greenrobot.org/greendao/ github网站: https://github.com/greenrobot/greenDAO greenDao Generator方式介绍: http://www.open-open.com/lib/view/open1438065400878.html 注释方式及增删改查详细介绍: http://www.jianshu.com/p/4e6d72e7f57a greenDao数据库存储路径修改: http://blog.csdn.net/chenzhenlindx/article/details/39183691 greenDao数据库升级: https://github.com/yuweiguocn/GreenDaoUpgradeHelper
源码下载
我的github地址:https://github.com/WJKCharlie/GreenDaoExample
前言 导入依赖和Schema设置 一、数据库模型生成及读取操作 二、修改数据库文件路径 三、获取加密的数据库 四、数据库升级又不删除数据 五、总结
前言: 在Android开发中,或多或少总要接触SQLite。然而在使用它时,我们往往需要做许多额外的工作,像编写 SQL 语句与解析查询结果等。所以,适用于 Android 的ORM 框架也就孕育而生了,现在市面上主流的框架有 OrmLite、SugarORM、Active Android、Realm 与 GreenDAO。而greenDAO号称是速度最快的ORM(见官网)。 简单的讲,greenDAO 是一个将对象映射到 SQLite 数据库中的轻量且快速的 ORM 解决方案。
下面,我将详解地介绍如何在 Android Studio 上使用 greenDAO,并结合代码总结一些使用过程中的心得。
导入依赖和Schema设置: 导入依赖和Schema参数设置详细内容可参见官网和如下网址: http://www.jianshu.com/p/4e6d72e7f57a 我的Schema设置如下:
greendao{ schemaVersion 1 targetGenDir 'src/main/java'}
一、数据库模型生成及读取操作: 1、greenDao支持的数据模型生成方式包括以下两种:(1)编写greendaoGenerator的纯Java类库,以生成DaoMaster、DaoSession、bean和beanDao等; 可参见: http://www.open-open.com/lib/view/open1438065400878.html 注:greenDao 3.0版本以下只能采用这种方式。在升级3.0以后,因支持了注释方式,该方式不再推荐。 (2)利用注释的方式,生成 DaoMaster、DaoSession、bean和beanDao等; 2、下文仅就注释方式生成流程进行简单的介绍,详细的讲解可参见 http://www.jianshu.com/p/4e6d72e7f57a 首先,新建datamodel包,用以包含 DaoMaster、DaoSession、bean和beanDao等。 然后新建Area实体类,代码如下:
@Entitypublic class Area { @Id private String AreaCode; private String AreaName;}
最后,Build->Make Module 'app',即可自动生成 DaoMaster、DaoSession、Area和AreaDao。此时Area实体类的代码如下: @Entitypublic class Area { @Id private String AreaCode; private String AreaName; @Generated(hash = 262290694) public Area(String AreaCode, String AreaName) { this.AreaCode = AreaCode; this.AreaName = AreaName; } @Generated(hash = 179626505) public Area() { } public String getAreaCode() { return this.AreaCode; } public void setAreaCode(String AreaCode) { this.AreaCode = AreaCode; } public String getAreaName() { return this.AreaName; } public void setAreaName(String AreaName) { this.AreaName = AreaName; }}
添加其他实体类的方法与Area一样。需要注意的是,不要手动修改 DaoMaster、DaoSession、bean和beanDao的代码,因为每一次编译项目,都会重新生成一次DaoMaster、DaoSession、bean和beanDao。如果修改的话,就会被覆盖掉。 为了便于数据的读取和添加,新建GreenDaoHelper辅助类,代码如下: public class GreenDaoHelper extends Application { private GreenDaoHelper Instance; private static DaoMaster daoMaster; private static DaoSession daoSession; public GreenDaoHelper getInstance() { if (Instance == null) { Instance = this; } return Instance; } /** * 获取DaoMaster * * @param context * @return */ public static DaoMaster getDaoMaster(Context context) { if (daoMaster == null) { try{ DaoMaster.OpenHelper helper = new DaoMaster.DevOpenHelper(context,"test.db",null); daoMaster = new DaoMaster(helper.getWritableDatabase()); //获取未加密的数据库 }catch (Exception e){ e.printStackTrace(); } } return daoMaster; } /** * 获取DaoSession对象 * * @param context * @return */ public static DaoSession getDaoSession(Context context) { if (daoSession == null) { if (daoMaster == null) { getDaoMaster(context); } daoSession = daoMaster.newSession(); } return daoSession; }}
在读写数据库之前,要添加读写权限:
在MainActivity.java中添加读写代码: public class MainActivity extends AppCompatActivity { private TextView textview; private DaoSession session; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textview=(TextView)findViewById(R.id.textview); session = GreenDaoHelper.getDaoSession(this); session.getAreaDao().deleteAll();//清空所有记录 //添加记录 Area area = new Area("01","北京"); Area area1 = new Area("02","天津"); session.getAreaDao().insert(area); session.getAreaDao().insert(area1); //查询记录 StringBuilder stringBuilder = new StringBuilder(); List areas = session.getAreaDao().loadAll(); for (int i = 0,n = areas.size();i
运行结果如下图所示: 二、修改数据库文件路径: 默认情况下,新创建的数据存储在data的包名目录下,设备如果不root的话,是无法查看SQLite数据库文件的。而实际应用中,我们往往需要copy数据库,或借用第三方工具查阅或编辑数据库内容。此时我们可以通过重写Context的 getDatabasePath( String name ) 、 openOrCreateDatabase(String name, int mode, CursorFactory factory) 、 openOrCreateDatabase(String name, int mode, CursorFactory factory, DatabaseErrorHandler errorHandler) 等三个方法 来修改SQLite文件的存储路径。 通过查询资料,发现 http://blog.csdn.net/chenzhenlindx/article/details/39183691 中的内容基本符合我们的需求。但是博主是在DaoMaster中重写方法的。通过上文我们知道,DaoMaster的代码是不能修改的。因此,我们可以将重写的方法放到GreenDaoHelper中去。 代码如下:
public class GreenDaoHelper extends Application { private GreenDaoHelper Instance; private static DaoMaster daoMaster; private static DaoSession daoSession; public GreenDaoHelper getInstance() { if (Instance == null) { Instance = this; } return Instance; } /** * 获取DaoMaster * * @param context * @return */ public static DaoMaster getDaoMaster(Context context) { if (daoMaster == null) { try{ ContextWrapper wrapper = new ContextWrapper(context) { /** * 获得数据库路径,如果不存在,则创建对象对象 * * @param name */ @Override public File getDatabasePath(String name) { // 判断是否存在sd卡 boolean sdExist = android.os.Environment.MEDIA_MOUNTED.equals(android.os.Environment.getExternalStorageState()); if (!sdExist) {// 如果不存在, Log.e("SD卡管理:", "SD卡不存在,请加载SD卡"); return null; } else {// 如果存在 // 获取sd卡路径 String dbDir = android.os.Environment.getExternalStorageDirectory().getAbsolutePath(); dbDir += "/Android";// 数据库所在目录 String dbPath = dbDir + "/" + name;// 数据库路径 // 判断目录是否存在,不存在则创建该目录 File dirFile = new File(dbDir); if (!dirFile.exists()) dirFile.mkdirs(); // 数据库文件是否创建成功 boolean isFileCreateSuccess = false; // 判断文件是否存在,不存在则创建该文件 File dbFile = new File(dbPath); if (!dbFile.exists()) { try { isFileCreateSuccess = dbFile.createNewFile();// 创建文件 } catch (IOException e) { e.printStackTrace(); } } else isFileCreateSuccess = true; // 返回数据库文件对象 if (isFileCreateSuccess) return dbFile; else return super.getDatabasePath(name); } } /** * 重载这个方法,是用来打开SD卡上的数据库的,android 2.3及以下会调用这个方法。 * * @param name * @param mode * @param factory */ @Override public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory) { return SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null); } /** * Android 4.0会调用此方法获取数据库。 * * @see android.content.ContextWrapper#openOrCreateDatabase(java.lang.String, * int, * android.database.sqlite.SQLiteDatabase.CursorFactory, * android.database.DatabaseErrorHandler) * @param name * @param mode * @param factory * @param errorHandler */ @Override public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory, DatabaseErrorHandler errorHandler) { return SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null); } }; DaoMaster.OpenHelper helper = new DaoMaster.DevOpenHelper(wrapper,"test.db",null); daoMaster = new DaoMaster(helper.getWritableDatabase()); //获取未加密的数据库 }catch (Exception e){ e.printStackTrace(); } } return daoMaster; } /** * 获取DaoSession对象 * * @param context * @return */ public static DaoSession getDaoSession(Context context) { if (daoSession == null) { if (daoMaster == null) { getDaoMaster(context); } daoSession = daoMaster.newSession(); } return daoSession; }}
此时,再运行上述代码,就会在Android目录下发现我们的test.db文件。通过第三方工具,即可查看我们的数据库内容。下图是我用手机端的SqliteLookup工具查看到的数据库内容: 三、获取加密的数据库: 修改GreenDaoHelper.java,通过调用DaoMaster.OpenHelper类的getEncryptedWritableDb(password)或者 getEncryptedReadableDb(password)方法, 即可获取加密的数据库。
public static DaoMaster getDaoMaster(Context context) { if (daoMaster == null) { try{ ContextWrapper wrapper = new ContextWrapper(context) { ... }; DaoMaster.OpenHelper helper = new DaoMaster.DevOpenHelper(wrapper,"test.db",null); daoMaster = new DaoMaster(helper.getEncryptedWritableDb("1234"));//获取加密的数据库 //daoMaster = new DaoMaster(helper.getEncryptedReadableDb("1234"));//获取加密的数据库 //daoMaster = new DaoMaster(helper.getWritableDatabase()); //获取未加密的数据库 }catch (Exception e){ e.printStackTrace(); } } return daoMaster;}
此时,若要解密或重新加密数据库,可参考 博客《利用SQLCipher加解密数据库(包括加解密已有的数据库)》。 四、数据库升级又不删除数据: 在实际开发的过程中,数据库的结构可能会有所改变。而 使用 DevOpenHelper 每次升级数据库时,表都会删除重建。因此,实际使用中需要建立类继承 DaoMaster.OpenHelper,实现 onUpgrade()方法。通过查询资料,对未加密的数据库,推荐使用 升级辅助库 GreenDaoUpgradeHelper( 可参见 https://github.com/yuweiguocn/GreenDaoUpgradeHelper/blob/master/README_CH.md )。该库通过 MigrationHelper在删表重建的过程中,使用临时表保存数据并还原。 示例程序 直接导入 MigrationHelper.java 源码。同时修改GreenDaoHelper.java文件,新建一个继承自 DaoMaster.OpenHelper的内部类MySQLiteOpenHelper。具体代码如下:
public class GreenDaoHelper extends Application { private GreenDaoHelper Instance; private static DaoMaster daoMaster; private static DaoSession daoSession; public GreenDaoHelper getInstance() { if (Instance == null) { Instance = this; } return Instance; } /** * 获取DaoMaster * * @param context * @return */ public static DaoMaster getDaoMaster(Context context) { if (daoMaster == null) { try{ ContextWrapper wrapper = new ContextWrapper(context) { ... }; DaoMaster.OpenHelper helper = new MySQLiteOpenHelper(wrapper,"test.db",null); //daoMaster = new DaoMaster(helper.getEncryptedWritableDb("1234"));//获取加密的数据库 //daoMaster = new DaoMaster(helper.getEncryptedReadableDb("1234"));//获取加密的数据库 daoMaster = new DaoMaster(helper.getWritableDatabase()); //获取未加密的数据库 }catch (Exception e){ e.printStackTrace(); } } return daoMaster; } /** * 获取DaoSession对象 * * @param context * @return */ public static DaoSession getDaoSession(Context context) { if (daoSession == null) { if (daoMaster == null) { getDaoMaster(context); } daoSession = daoMaster.newSession(); } return daoSession; } private static class MySQLiteOpenHelper extends DaoMaster.OpenHelper { public MySQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) { super(context, name, factory); } private static final String UPGRADE="upgrade"; @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { MigrationHelper.migrate(db,AreaDao.class); Log.e(UPGRADE,"upgrade run success"); } }}
另外添加一个People实体类,并修改schemaVersion为更高的版本号,然后 Build->Make Module 'app',生成新的模型类。修改OnUpgrade方法如下: @Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { MigrationHelper.migrate(db,AreaDao.class, PeopleDao.class); Log.e(UPGRADE,"upgrade run success");}
此时,运行程序。虽然程序成功启动,但是报如下错误: 12-30 10:02:12.503 6312-6312/com.wjk.greendaoexample E/SQLiteLog: (1) no such table: PEOPLE12-30 10:02:12.508 6312-6312/com.wjk.greendaoexample E/MigrationHelper: 【Failed to generate temp table】PEOPLE_TEMP android.database.sqlite.SQLiteException: no such table: PEOPLE (code 1): , while compiling: CREATE TEMPORARY TABLE PEOPLE_TEMP AS SELECT * FROM PEOPLE;
通过阅读源码发现,程序根据传入的beanDao对 所有 bean表都创建了临时表,并从bean表复制数据到bean_temp表中。而此时,People实体是新创建的,数据库中并没有这个表,因此报上面的错误。此时,我们需对源码进行修改,仅对数据库中已有的表创建临时表并保存数据。此外,源码中是按字段恢复数据,为方便起见,本程序修改为全表查询恢复。代码如下: public final class MigrationHelper { public static boolean DEBUG = false; private static String TAG = "MigrationHelper"; private static List tablenames = new ArrayList<>(); public static List getTables(SQLiteDatabase db){ List tables = new ArrayList<>(); Cursor cursor = db.rawQuery("select name from sqlite_master where type='table' order by name", null); while(cursor.moveToNext()){ //遍历出表名 tables.add(cursor.getString(0)); } cursor.close(); return tables; } public static void migrate(SQLiteDatabase db, Class<? extends AbstractDao<?, ?>>... daoClasses) { Database database = new StandardDatabase(db); if (DEBUG) { Log.d(TAG, "【Database Version】" + db.getVersion()); Log.d(TAG, "【Generate temp table】start"); } tablenames=getTables(db); generateTempTables(database, daoClasses); if (DEBUG) { Log.d(TAG, "【Generate temp table】complete"); } dropAllTables(database, true, daoClasses); createAllTables(database, false, daoClasses); if (DEBUG) { Log.d(TAG, "【Restore data】start"); } restoreData(database, daoClasses); if (DEBUG) { Log.d(TAG, "【Restore data】complete"); } } private static void generateTempTables(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) { for (int i = 0; i < daoClasses.length; i++) { String tempTableName = null; try { DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]); if(!tablenames.contains(daoConfig.tablename)){//如果数据库中没有该表,则继续下次循环 continue; } String tableName = daoConfig.tablename; tempTableName = daoConfig.tablename.concat("_TEMP"); StringBuilder dropTableStringBuilder = new StringBuilder(); dropTableStringBuilder.append("DROP TABLE IF EXISTS ").append(tempTableName).append(";"); db.execSQL(dropTableStringBuilder.toString()); StringBuilder insertTableStringBuilder = new StringBuilder(); insertTableStringBuilder.append("CREATE TEMPORARY TABLE ").append(tempTableName); insertTableStringBuilder.append(" AS SELECT * FROM ").append(tableName).append(";"); db.execSQL(insertTableStringBuilder.toString()); if (DEBUG) { Log.d(TAG, "【Table】" + tableName +"\n ---Columns-->"+getColumnsStr(daoConfig)); Log.d(TAG, "【Generate temp table】" + tempTableName); } } catch (SQLException e) { Log.e(TAG, "【Failed to generate temp table】" + tempTableName, e); } } } private static String getColumnsStr(DaoConfig daoConfig) { if (daoConfig == null) { return "no columns"; } StringBuilder builder = new StringBuilder(); for (int i = 0; i < daoConfig.allColumns.length; i++) { builder.append(daoConfig.allColumns[i]); builder.append(","); } if (builder.length() > 0) { builder.deleteCharAt(builder.length() - 1); } return builder.toString(); } private static void dropAllTables(Database db, boolean ifExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) { reflectMethod(db, "dropTable", ifExists, daoClasses); if (DEBUG) { Log.d(TAG, "【Drop all table】"); } } private static void createAllTables(Database db, boolean ifNotExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) { reflectMethod(db, "createTable", ifNotExists, daoClasses); if (DEBUG) { Log.d(TAG, "【Create all table】"); } } /** * dao class already define the sql exec method, so just invoke it */ private static void reflectMethod(Database db, String methodName, boolean isExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) { if (daoClasses.length < 1) { return; } try { for (Class cls : daoClasses) { Method method = cls.getDeclaredMethod(methodName, Database.class, boolean.class); method.invoke(null, db, isExists); } } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } private static void restoreData(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) { for (int i = 0; i < daoClasses.length; i++) { String tempTableName = null; try { DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]); String tableName = daoConfig.tablename; if(!tablenames.contains(tableName)){ continue; } tempTableName = daoConfig.tablename.concat("_TEMP"); StringBuilder insertTableStringBuilder = new StringBuilder(); insertTableStringBuilder.append("INSERT INTO ").append(tableName).append(" SELECT * FROM ").append(tempTableName).append(";"); db.execSQL(insertTableStringBuilder.toString()); if (DEBUG) { Log.d(TAG, "【Restore data】 to " + tableName); } StringBuilder dropTableStringBuilder = new StringBuilder(); dropTableStringBuilder.append("DROP TABLE IF EXISTS ").append(tempTableName); db.execSQL(dropTableStringBuilder.toString()); if (DEBUG) { Log.d(TAG, "【Drop temp table】" + tempTableName); } } catch (SQLException e) { Log.e(TAG, "【Failed to restore data from temp table (probably new table)】" + tempTableName, e); } } }}
此时,新建一个实体类Product,修改版本号,同时 修改OnUpgrade方法 , 再次运行程序,则成功运行。 注意:MigrationHelper.migrate(),暂时只接收 SQLiteDatabase ,不接收 Database,且对 加密的 数据库是无效的。而实际应用中,由于数据的重要性,数据库往往是必须要加密的。因此,对于加密的 数据库该如何进行更新支持呢? 具体思路如下,首先分析 MigrationHelper.migrate() 为什么不支持 对 加密的 数据库的更新,然后再找出对应的解决方案。 (1)MigrationHelper.migrate()为什么不支持对加密的数据库的更新 通过加断点调试发现,数据库更新时并没有走MySQLiteOpenHelper的onUpgrade()方法,而是走的DatabaseOpenHelper抽象类里的EncryptedHelper内部类的 onUpgrade()方法。如图所示: EncryptedHelper内部类的 onUpgrade()方法调用的是 DatabaseOpenHelper抽象类本身的 onUpgrade()方法,但DatabaseOpenHelper抽象类本身的 onUpgrade()方法默认是啥也不执行的,如下图所示。因此, MigrationHelper.migrate() 为什么不支持 对 加密的 数据库的更新也就显而易见了。
(2)对加密的数据库的更新支持的解决方案 此时,可以通过修改greenDao的源码,在 EncryptedHelper内部类的 onUpgrade()方法中添加对 MigrationHelper.migrate()的调用。 但,如果我不想修改源码,该怎么解决呢?默认情况下,获取和更新加密的数据库,调用的是 DatabaseOpenHelper抽象类本身的getEncryptedWritableDb(String password)和 onUpgrade()方法 。此时,我们可以在GreenDaoHelper中定义一个新的类MyEncryptedSQLiteOpenHelper继承自DaoMaster.OpenHelper,并在这个类中对这两个方法进行重写,同时在内部自定义一个net.sqlcipher.database.SQLiteOpenHelper的继承类,以代替 DatabaseOpenHelper抽象类里的EncryptedHelper内部类。 在添加代码之前,要先添加对sqlcipher的依赖,如下:
compile 'net.zetetic:android-database-sqlcipher:3.5.4@aar'
具体代码如下: public class GreenDaoHelper extends Application { ...... public static DaoMaster getDaoMaster(Context context) { if (daoMaster == null) { try{ ContextWrapper wrapper = new ContextWrapper(context) { ... }; //DaoMaster.OpenHelper helper = new MySQLiteOpenHelper(wrapper,"test.db",null); MyEncryptedSQLiteOpenHelper helper = new MyEncryptedSQLiteOpenHelper(wrapper,"test.db",null); daoMaster = new DaoMaster(helper.getEncryptedWritableDb("1234"));//获取加密的数据库 //daoMaster = new DaoMaster(helper.getEncryptedReadableDb("1234"));//获取加密的数据库 //daoMaster = new DaoMaster(helper.getWritableDatabase()); //获取未加密的数据库 }catch (Exception e){ e.printStackTrace(); } } return daoMaster; } ...... private static class MyEncryptedSQLiteOpenHelper extends DaoMaster.OpenHelper { private final Context context; private final String name; private final int version = DaoMaster.SCHEMA_VERSION; private boolean loadSQLCipherNativeLibs = true; public MyEncryptedSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) { super(context, name, factory); this.context=context; this.name=name; } private static final String UPGRADE="upgrade"; @Override public void onUpgrade(Database db, int oldVersion, int newVersion) { EncryptedMigrationHelper.migrate((EncryptedDatabase) db,AreaDao.class, PeopleDao.class, ProductDao.class); Log.e(UPGRADE,"upgrade run success"); } @Override public Database getEncryptedWritableDb(String password) { MyEncryptedHelper encryptedHelper = new MyEncryptedHelper(context,name,version,loadSQLCipherNativeLibs); return encryptedHelper.wrap(encryptedHelper.getReadableDatabase(password)); } private class MyEncryptedHelper extends net.sqlcipher.database.SQLiteOpenHelper { public MyEncryptedHelper(Context context, String name, int version, boolean loadLibs) { super(context, name, null, version); if (loadLibs) { net.sqlcipher.database.SQLiteDatabase.loadLibs(context); } } @Override public void onCreate(net.sqlcipher.database.SQLiteDatabase db) { MyEncryptedSQLiteOpenHelper.this.onCreate(wrap(db)); } @Override public void onUpgrade(net.sqlcipher.database.SQLiteDatabase db, int oldVersion, int newVersion) { MyEncryptedSQLiteOpenHelper.this.onUpgrade(wrap(db), oldVersion, newVersion); } @Override public void onOpen(net.sqlcipher.database.SQLiteDatabase db) { MyEncryptedSQLiteOpenHelper.this.onOpen(wrap(db)); } protected Database wrap(net.sqlcipher.database.SQLiteDatabase sqLiteDatabase) { return new EncryptedDatabase(sqLiteDatabase); } } }}
上述代码中引用了EncryptedMigrationHelper.java类。该类与 MigrationHelper.java类似,只不过将android.database.sqlite.SQLiteDatabase替换为net.sqlcipher.database.SQLiteDatabase,同时对代码做了微小的改动。 EncryptedMigrationHelper.java类的代码如下: public class EncryptedMigrationHelper { public static boolean DEBUG = true; private static String TAG = "UpgradeHelper"; private static List tablenames = new ArrayList<>(); public static List getTables(SQLiteDatabase db){ List tables = new ArrayList<>(); Cursor cursor = db.rawQuery("select name from sqlite_master where type='table' order by name", null); while(cursor.moveToNext()){ //遍历出表名 tables.add(cursor.getString(0)); } cursor.close(); return tables; } public static void migrate(EncryptedDatabase db, Class<? extends AbstractDao<?, ?>>... daoClasses) { Database database = db; if (DEBUG) { Log.d(TAG, "【Database Version】" + db.getSQLiteDatabase().getVersion()); Log.d(TAG, "【Generate temp table】start"); } tablenames=getTables(db.getSQLiteDatabase()); generateTempTables(database, daoClasses); if (DEBUG) { Log.d(TAG, "【Generate temp table】complete"); } dropAllTables(database, true, daoClasses); createAllTables(database, false, daoClasses); if (DEBUG) { Log.d(TAG, "【Restore data】start"); } restoreData(database, daoClasses); if (DEBUG) { Log.d(TAG, "【Restore data】complete"); } } private static void generateTempTables(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) { for (int i = 0; i < daoClasses.length; i++) { String tempTableName = null; try { DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]); if(!tablenames.contains(daoConfig.tablename)){ continue; } String tableName = daoConfig.tablename; tempTableName = daoConfig.tablename.concat("_TEMP"); StringBuilder dropTableStringBuilder = new StringBuilder(); dropTableStringBuilder.append("DROP TABLE IF EXISTS ").append(tempTableName).append(";"); db.execSQL(dropTableStringBuilder.toString()); StringBuilder insertTableStringBuilder = new StringBuilder(); insertTableStringBuilder.append("CREATE TEMPORARY TABLE ").append(tempTableName); insertTableStringBuilder.append(" AS SELECT * FROM ").append(tableName).append(";"); db.execSQL(insertTableStringBuilder.toString()); if (DEBUG) { Log.d(TAG, "【Table】" + tableName +"\n ---Columns-->"+getColumnsStr(daoConfig)); Log.d(TAG, "【Generate temp table】" + tempTableName); } } catch (SQLException e) { Log.e(TAG, "【Failed to generate temp table】" + tempTableName, e); } } } private static String getColumnsStr(DaoConfig daoConfig) { if (daoConfig == null) { return "no columns"; } StringBuilder builder = new StringBuilder(); for (int i = 0; i < daoConfig.allColumns.length; i++) { builder.append(daoConfig.allColumns[i]); builder.append(","); } if (builder.length() > 0) { builder.deleteCharAt(builder.length() - 1); } return builder.toString(); } private static void dropAllTables(Database db, boolean ifExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) { reflectMethod(db, "dropTable", ifExists, daoClasses); if (DEBUG) { Log.d(TAG, "【Drop all table】"); } } private static void createAllTables(Database db, boolean ifNotExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) { reflectMethod(db, "createTable", ifNotExists, daoClasses); if (DEBUG) { Log.d(TAG, "【Create all table】"); } } /** * dao class already define the sql exec method, so just invoke it */ private static void reflectMethod(Database db, String methodName, boolean isExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) { if (daoClasses.length < 1) { return; } try { for (Class cls : daoClasses) { Method method = cls.getDeclaredMethod(methodName, Database.class, boolean.class); method.invoke(null, db, isExists); } } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } private static void restoreData(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) { for (int i = 0; i < daoClasses.length; i++) { String tempTableName = null; try { DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]); String tableName = daoConfig.tablename; if(!tablenames.contains(tableName)){ continue; } tempTableName = daoConfig.tablename.concat("_TEMP"); StringBuilder insertTableStringBuilder = new StringBuilder(); insertTableStringBuilder.append("INSERT INTO ").append(tableName).append(" SELECT * FROM ").append(tempTableName).append(";"); db.execSQL(insertTableStringBuilder.toString()); if (DEBUG) { Log.d(TAG, "【Restore data】 to " + tableName); } StringBuilder dropTableStringBuilder = new StringBuilder(); dropTableStringBuilder.append("DROP TABLE IF EXISTS ").append(tempTableName); db.execSQL(dropTableStringBuilder.toString()); if (DEBUG) { Log.d(TAG, "【Drop temp table】" + tempTableName); } } catch (SQLException e) { Log.e(TAG, "【Failed to restore data from temp table (probably new table)】" + tempTableName, e); } } }}
通过加断点调试发现,数据库更新时调用了MyEncryptedSQLiteOpenHelper的onUpgrade()方法 。如图所示: 至此,我们已能够对加密的数据库进行更新支持。
五、总结: 该博客对greenDao模型生成、增删改查、存储路径 修改 、加解密及更新升级等都有了较为详细的描述,在实际开发中也基本够用。不过,需要注意的是,本文引用的EncryptedMigrationHelper.java类和 MigrationHelper.java类均只支持对数据库对象的新增和删除,对于对象字段有修改的情况没做考虑。如果读者有需要的话,可另行开发。
参考文献如下: 官网: http://greenrobot.org/greendao/ github网站: https://github.com/greenrobot/greenDAO greenDao Generator方式介绍: http://www.open-open.com/lib/view/open1438065400878.html 注释方式及增删改查详细介绍: http://www.jianshu.com/p/4e6d72e7f57a greenDao数据库存储路径修改: http://blog.csdn.net/chenzhenlindx/article/details/39183691 greenDao数据库升级: https://github.com/yuweiguocn/GreenDaoUpgradeHelper
源码下载
我的github地址:https://github.com/WJKCharlie/GreenDaoExample
更多相关文章
- androidRSA加密,java解密出现错误或者乱码
- 如何Android数据库缓存进行管理
- Android(安卓)数据库开发之事务
- Android简易实战教程--第四十七话《使用OKhttp回调方式获取网络
- Android获取View的宽度和高度
- Android(安卓)sdcard媒体文件更新(程序控制刷新MediaStore数据库)
- 写了个Android聊天客户端框架,基本聊天功能、数据库、服务器都有
- Android(安卓)字体国际化适配方法以及源码解析
- Apk脱壳圣战之---脱掉“爱加密”的壳