android_sqlite数据库从基础到SDK封装
来源:互联网 发布:网络侵犯名誉权 编辑:程序博客网 时间:2024/05/21 08:51
本篇从数据库的基本使用开始,然后利用反射+泛型对数据库操作进行优化,批量插入操作使用事务提高效率,采用链式调用方式优化查询操作。
最后是封装一个高可用,易替换,不受框架影响的数据库操作SDK。
一、SQLite基本使用
1.1 数据库及表的创建
public class DbHelper extends SQLiteOpenHelper { private static final int VERSION = 1; private static final String DB_NAME = "person.db"; public static final String TABLE_NAME = "person"; private static final String CREATE_TABLE_SQL = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (id integer primary key autoincrement, name text, age integer);"; public DbHelper(Context context) { super(context, DB_NAME, null, VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_TABLE_SQL); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } }
1.2 数据库操作的实现(增、删、改、查)
public class DbManager { private static DbManager sDbManager; private DbHelper mDbHelper; private DbManager(Context context) { mDbHelper = new DbHelper(context); } public static DbManager getInstance(Context context) { if (sDbManager == null) { synchronized (DbManager.class) { if (sDbManager == null) { sDbManager = new DbManager(context); } } } return sDbManager; } public long insert(Person person) { ContentValues values = contentValuesByObj(person); SQLiteDatabase database = mDbHelper.getWritableDatabase(); return database.insert(DbHelper.TABLE_NAME, null, values); } public int delete(String whereClause, String...whereArgs) { SQLiteDatabase database = mDbHelper.getWritableDatabase(); return database.delete(DbHelper.TABLE_NAME, whereClause, whereArgs); } public int update(Person person, String whereClause, String... whereArgs) { SQLiteDatabase database = mDbHelper.getWritableDatabase(); ContentValues values = contentValuesByObj(person); return database.update(DbHelper.TABLE_NAME, values, whereClause, whereArgs); } public List<Person> queryAll() { return query(null, null, null, null, null, null, null); } public List<Person> query(String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { SQLiteDatabase database = mDbHelper.getWritableDatabase(); Cursor cursor = database.query(DbHelper.TABLE_NAME, columns, selection, selectionArgs, groupBy, having, orderBy, limit); if (cursor != null && cursor.moveToFirst()) { List<Person> persons = new ArrayList<>(); do { int idColumnIndex = cursor.getColumnIndex("id"); int nameColumnIndex = cursor.getColumnIndex("name"); int ageColumnIndex = cursor.getColumnIndex("age"); int id = cursor.getInt(idColumnIndex); String name = cursor.getString(nameColumnIndex); int age = cursor.getInt(ageColumnIndex); Person person = new Person(id, name, age); persons.add(person); } while (cursor.moveToNext()); return persons; } return null; } private ContentValues contentValuesByObj(Person person) { ContentValues values = new ContentValues(); if (person.getId() != 0) { values.put("id", person.getId()); } values.put("name", person.getName()); values.put("age", person.getAge()); return values; } }
1.3 测试
public class MainActivity extends AppCompatActivity { @ViewById(R.id.tv) private TextView mTv; @ViewById(R.id.input_et) private EditText mInputEt; private DbManager mDbManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ViewUtils.inject(this); mDbManager = DbManager.getInstance(this); } @OnClick(R.id.btn) private void click() { Toast.makeText(MainActivity.this, mTv.getText().toString(), Toast.LENGTH_SHORT).show(); } @OnClick(R.id.add) private void add() { Person person = new Person("dapan", new Random().nextInt(30)); long insert = mDbManager.insert(person); if (insert != -1) { Toast.makeText(MainActivity.this, "插入成功: " + insert, Toast.LENGTH_SHORT).show(); } } @OnClick(R.id.del) private void del() { int delete = mDbManager.delete("name=?", "dapan"); Toast.makeText(MainActivity.this, "删除成功: " + delete, Toast.LENGTH_SHORT).show(); } @OnClick(R.id.update) private void update() { Person person = new Person("dapan", 29); int update = mDbManager.update(person, "id=?", "1"); Toast.makeText(MainActivity.this, "更新成功: " + update, Toast.LENGTH_SHORT).show(); } @OnClick(R.id.query) private void query() { List<Person> persons = mDbManager.queryAll(); if (persons == null || persons.size() <=0) { Toast.makeText(MainActivity.this, "没有数据! ", Toast.LENGTH_SHORT).show(); return; } Toast.makeText(MainActivity.this, "person count: " + persons.size(), Toast.LENGTH_SHORT).show(); } }
通过以上三步,就可以在我们的应用中使用SQLite数据库了。
这样就能用?的确,如果不考虑易用性,不考虑性能、不考虑封装,使用上面的三步操作,就可以交差了。
但是,我想说,如果真这样做了,那你要对自己的态度负责,为什么呢?实际开发中,不可能只有Person这一张表,有没有考虑过数据量很大的情况?在有多张表时,你可能会这样做:
- 修改DbHelper,添加新表的创建语句;
- 再写一个XxxDbManager,然后里面的insert(Xxx xxx),这一系列方法都要重写;
- 。。。
难用,不易扩展,要是用新的框架替代又是麻烦事,下面,将祭出我的第一个修改版。
二、利用反射+泛型优化contentValues与对象互转操作
初次使用SQLite时,提到,在插入操作时,参数类型是Person,也就是说写死的,如果在换一个,Student,那插入操作就不能用了,有没有什么好的办法呢?当然有,那就是泛型!如果在我们的插入操作,不关注具体数据类型,那就不会有这个问题了,说的容易,做起来呢?也容易!但,前提是,你能把反射用的很6。
好,先来回顾一下,我们之前是如何插入Person数据的:
public long insert(Person person) { ContentValues values = contentValuesByObj(person); SQLiteDatabase database = mDbHelper.getWritableDatabase(); return database.insert(DbHelper.TABLE_NAME, null, values); } private ContentValues contentValuesByObj(Person person) { ContentValues values = new ContentValues(); if (person.getId() != 0) { values.put("id", person.getId()); } values.put("name", person.getName()); values.put("age", person.getAge()); return values; }
还不错,哈哈,为什么这样说呢?因为插入、修改操作,都要将Obj->ContentVaules,我们将这部分抽取成一个方法,这样,也是一种封装嘛,免得,要写重复的代码!
但,这是这样的封装,只对一个类型有用,如果像上面说的增加新表Student,那就不好使了,所以再次封装时,要考虑到扩展性,那我们就从参数类型着手,不管你要插入什么数据,我们支持,就是这么牛X!
来吧,给你反射,好好的玩耍〜〜
在我们的DbManager.java中添加新的contentValuesByObj方法,这里的Obj不是具体的某个对象,是一个泛型T,所以我们的新方法名为:contentValuesByT。
private <T> ContentValues contentValuesByT(T obj) { ContentValues values = new ContentValues(); Field[] fields = obj.getClass().getDeclaredFields(); try { for (Field field : fields) { field.setAccessible(true); String name = field.getName(); //name = $change, type = IncrementalChange if (field.isSynthetic() || "serialVersionUID".equals(name)) { continue; } Object value = field.get(name); // 利用反射,获取ContentValues中的put方法, // getDeclaredMethod: // 第一个参数:方法名 // 第二个参数:put方法的第一个参数 // 第三个参数:put方法的第二个参数 Method putMethod = ContentValues.class.getDeclaredMethod("put", String.class, value.getClass()); //通过反射,执行方法,就像:values.put(name, value); putMethod.invoke(values, name, value); } } catch (Exception e) { e.printStackTrace(); } return values; }
为什么要这么麻烦的反射ContentValues中的put方法呢?
我们看下它的源码就知道了:
public void put(String key, String value) { mValues.put(key, value); } public void put(String key, Integer value) { mValues.put(key, value); } public void put(String key, Long value) { mValues.put(key, value); }
其它的与此类似,这里只取三个,现在知道,为什么通过反射拿到属性名,然后根据获取对应的值,去反射ContentValues里的put方法了吧?,就是为了获取不同value类型的put方法!
别看这个方法只用在insert/update两个,而且只修改了一行代码,但是,就是这个修改,让我们的数据库与具体数据类型完全解耦,从此不用去管如何将对象转为ContentValues啦!
三、反射+泛型+链式调用=更好的查询操作
上面对我们的insert/update操作,做了一个小的改进,我们继续!
下面开始操刀,处理一下查询操作。查询是这四种操作里面,最复杂的一个,没有之一!它的条件多的令人发指,让我们从易用性的角度优化这个查询操作。show me code!
首先,我们看下SQLiteDataBase中的一个query方法:
public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { return query(false, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit); }
没错,就是八个参数!
我们要记住每个位置上的参数代表什么含意,不然,系统就会报错给你看!太可怕了,那我们怎么优化呢?通常情况下,我们是不需要八个参数都设置一遍的。嵌套调用这种方式是行不通的,参数太多,这样要写多少个方法?有一种方式,最适合这种多参数的场景,那就是 链式调用 ,我们为每个参数添加一个方法,如果用户没有调用,设置为默认的null即可,这样,需要什么参数,调用就好。是不是 so easy,再也不怕记错参数位置啦!
我们不仅要考虑查询使用的优化,对于代码,我们也要严格要求自己,如果将这么多的方法写到一个类中,易读性明显变差,所以,我们创建一个新的QueryCenter类,没错,它就是专门处理查询的。
public class QueryCenter<T> { private String[] columns; private String selection; private String[] selectionArgs; private String groupBy; private String having; private String orderBy; private String limit; public QueryCenter<T> setColumns(String[] columns) { this.columns = columns; return this; } public QueryCenter<T> setSelection(String selection) { this.selection = selection; return this; } public QueryCenter<T> setSelectionArgs(String[] selectionArgs) { this.selectionArgs = selectionArgs; return this; } public QueryCenter<T> setGroupBy(String groupBy) { this.groupBy = groupBy; return this; } public QueryCenter<T> setHaving(String having) { this.having = having; return this; } public QueryCenter<T> setOrderBy(String orderBy) { this.orderBy = orderBy; return this; } public QueryCenter<T> setLimit(String limit) { this.limit = limit; return this; } }
上面只是将参数封装成了方法,下面,我们完善query()方法:
public QueryCenter(SQLiteDatabase database, T obj) { this.mDatabase = database; this.obj = obj; } public List<T> queryAll() { String tableName = DbUtil.getTableName(obj.getClass()); Cursor cursor = mDatabase.query(tableName, null, null, null, null, null, null); return listByCursor(cursor); } public List<T> query() { String tableName = DbUtil.getTableName(obj.getClass()); Cursor cursor = mDatabase.query(tableName, columns, selection, selectionArgs, groupBy, having, orderBy, limit); clearParams(); // 每次查询完了,将所有参数设置为null return cursor2List(cursor); } private List<T> cursor2List(Cursor cursor) { return null; }
先根据需求设置参数,然后调用query/queryAll()即可完成查询!查询结果返回给我们一个Cursor,最后处理一下这个Cursor,我们的查询方法就算大功告成啦!开心〜
private List<T> cursor2List(Cursor cursor) { List<T> list = new ArrayList<>(); if (cursor != null && cursor.moveToFirst()) { try { do { T instance = mClazz.newInstance(); Field[] fields = mClazz.getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); String name = field.getName(); int columnIndex = cursor.getColumnIndex(name); // cursor.getString(columnIndex); if (columnIndex == -1) { continue; } Method cursorMethod = cursorMethod(field.getType()); if (cursorMethod != null) { Object value = cursorMethod.invoke(cursor, columnIndex); if (value == null) { continue; } value = DbUtil.parseTypeValue(field, value); field.set(instance, value); } list.add(instance); } // for end } while (cursor.moveToNext()); } catch (Exception e) { e.printStackTrace(); } } return list; } private Method cursorMethod(Class<?> type) throws NoSuchMethodException { String methodName = getColumnMethodName(type); Method method = Cursor.class.getDeclaredMethod(methodName, int.class); return method; } private String getColumnMethodName(Class<?> fieldType) { String typeName = null; if (fieldType.isPrimitive()) { //将基本数据类型的首字母大写 typeName = DbUtil.capitalize(fieldType.getName()); } else { typeName = fieldType.getSimpleName(); } String methodName = "get" + typeName; if ("getBoolean".equals(methodName)) { methodName = "getInt"; } else if ("getChar".equals(methodName) || "getCharacter".equals(methodName)) { methodName = "getString"; } else if ("getDate".equals(methodName)) { methodName = "getLong"; } else if ("getInteger".equals(methodName)) { methodName = "getInt"; } return methodName; }
这部分非常关键,我们的思路是,使用newInstance()来实例化对象,利用反射获取columnIndex列的值,最后调用该属性的field.set()方法,将该列的值赋值给instance对象。
四、自动化建表
首先,我们来分析一下,通常情况下,数据库只不过是存储的一张张的表,而每一张表,通常情况下对应一个实体类,从这点出发,我们是不是考虑,将创建数据库表这第一层来个封装呢?我们来试着做一下:
private static final String CREATE_TABLE_SQL = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (id integer primary key autoincrement, name text, age integer);";
从上面的建表语句,我们可知,变化的有两部分,第一:表名–>实体类名;第二:字段名–>实体类中的成员属性。
类名,很容易获得,但,成员属性就难了,我们根本不知道下一张表内会有哪些内容!怎么定?!
在java中有一个“旁门左道”-反射,为什么说它是“旁门左道”呢?因为,它将破坏封装的规则,如果:有一个属性,这是类内部使用的,但,只要通过反射,就能拿到这个属性,就能随意赋值了!一个方法,本来不需要外部知道,即使是private,但通过反射就能执行这个方法!
4.0 前提
在上面分析了建表语句的”变”与”不变”部分,还有一点需要注意,那就是java中的类型与sqlite中的数据类型的差异。比如:int->integer, String->text, sqlite中没有boolean这数据类型。
常见的数据类型之间的关系如下:
java sqlite ======================= String text int integer boolean boolean float float double double char varchar long long
注:因为java和sqlite中的数据类型都是固定的,其他的,自行添加即可(这里主要想说明这种套路!)。
有了上面的这个表,那我们该怎么利用它呢?
4.1 类型转换
网上其实很多文章介绍反射的,我在从android:onClick属性谈运行时注解在Android中的运用中利用反射获取属性上的注解,让“劳力士”帮咱完成了findViewById()操作。这里呢,我们还要让它上,让它帮我们偷取属性,然后合成建表语句。
创建工作类DaoUtil:
public class DaoUtil { private DaoUtil() { throw new UnsupportedOperationException("cannot be instantiated"); } public static <T> String getTableName(Class<T> clazz) { return clazz.getSimpleName(); } public static String getColumnType(String type) { String value = null; if (type.contains("String")) { value = " text"; } else if (type.contains("int")) { value = " integer"; } else if (type.contains("boolean")) { value = " boolean"; } else if (type.contains("float")) { value = " float"; } else if (type.contains("double")) { value = " double"; } else if (type.contains("char")) { value = " varchar"; } else if (type.contains("long")) { value = " long"; } else { value = " " + type; } return value; } }
4.2 生成建表语句
/** * "CREATE TABLE IF NOT EXISTS person (id integer primary key autoincrement, name text, age integer);"; */ public static <T> String generatorCreateTableSql(T obj) { StringBuilder sql = new StringBuilder("CREATE TABLE IF NOT EXISTS "); sql.append(getTableName(obj.getClass())); sql.append(" (id integer primary key autoincrement, "); //获取属性,合成表中的必要字段 Field[] fields = obj.getClass().getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); String name = field.getName(); String type = field.getType().getSimpleName(); //name = $change, type = IncrementalChange if (field.isSynthetic() || "serialVersionUID".equals(name)) { continue; } sql.append(name).append(getColumnType(type)).append(", "); } sql.replace(sql.length() - 2, sql.length(), ");"); //用");"替掉最后一个", " return sql.toString(); }
这里,我们直接使用反射获取属性,然后将java中的数据类型转为sqlite中的数据类型,然后拼接生成建表语句。需要注意的是,我在as中,编译后,会生成两个与所生成数据库表无关的字段:
name = $changename = serialVersionUID
我们这里不需要这两个属性参与,遇到直接continue过滤掉。
至此,我们可以自动化生成数据库表了,怎么样,又可以多刷两分钟的微博啦〜
五、跟着源码优化反射操作
通读全篇,我们有三处使用了反射:
- 生成建表语句
- 对象转ContentValues
- 查询结果Cursor转List
=======
5.1 优化contentValuesByObj方法
private static final Object[] sPutMethodArgs = new Object[2]; // 缓存,避免不必要的创建 private static final Map<String, Method> sPutMethod = new ArrayMap<String, Method>(); //缓存方法 private <T> ContentValues contentValuesByTFinal(T obj) { ContentValues values = new ContentValues(); Field[] fields = obj.getClass().getDeclaredFields(); try { for (Field field : fields) { field.setAccessible(true); String name = field.getName(); //name = $change, type = IncrementalChange if (field.isSynthetic() || "serialVersionUID".equals(name)) { continue; } Object value = field.get(obj); sPutMethodArgs[0] = name; sPutMethodArgs[1] = value; String typeName = field.getType().getSimpleName(); Method putMethod = sPutMethod.get(typeName); if (putMethod == null) { putMethod = ContentValues.class.getDeclaredMethod("put", String.class, value.getClass()); sPutMethod.put(typeName, putMethod); } //通过反射,执行方法,就像:values.put(name, value); putMethod.invoke(values, sPutMethodArgs); } } catch (Exception e) { e.printStackTrace(); } finally { sPutMethodArgs[0] = null; sPutMethodArgs[1] = null; } return values; } ```这里对方法的参数、反射得到的方法进行了缓存,避免不必要的对象创建过程。这里参考的来自support.v7中的[AppCompatViewInflater.java#createViewFromTag()](http://androidxref.com/7.1.1_r6/xref/frameworks/support/v7/appcompat/src/android/support/v7/app/AppCompatViewInflater.java) 源码。### 5.2 优化批量插入操作这个很简单,就是在插入之前,开启事务。
public void insert(List<Person> list) { mDbHelper.getWritableDatabase().beginTransaction(); for (Person obj : list) { insert(obj); } mDbHelper.getWritableDatabase().setTransactionSuccessful(); mDbHelper.getWritableDatabase().endTransaction();}
当然,这只是最基础的优化,我们要考虑到,如果只是数据量大,前后关系不是很密切,那应该考虑分批插入等,需要根据实际应用场景进行扩展,当然,等你的应用发展到一定规模,只是这么做还是存在很多风险的,所以,我们要从全局去思考,也许,这是我们走上架构的第一步。。。# 六、全局思考说了这么多,万一我用了这个框架,万一又出问题了,或者领导要求使用比较成熟的其他第三方数据库框架,那怎么办?系统小了还好,删除与此相关的引用,然后使用新的数据库框架即可,但是,要是一个App有很多地方引用,或者多个App都用这个,那太可怕了,所以,从全局思考,我们该如何使用数据库框架来优化我们的应用架构呢?或者说,可以尽可能的减少我们的维护成本呢?很显示,那就是借助在使用任何第三方框架时,先将它封装一层,让第三方框架与我们的应用隔离。在App中,使用我们封装的那一层,比如:db.insert(), db.update(),db.query(),这样一来,就降低了App与第三方框架的耦合,怎么样?现在开始封装我们的SDK吧!这里我们使用一种工厂方式模式,来搭建我们的数据库SDK。![anKataLite/db_sdk框架图](http://img.blog.csdn.net/20170326213514143?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZGFwYW43Mjg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)### 6.1 创建统一的操作归范
public interface IDbSupport<T> { long insert(T obj); int delete(String whereClause, String... whereArgs); int update(T obj, String whereClause, String... whereArgs); QueryCenter getQuery(Class<?> clazz); void setDbHelperAndClazz(DbHelper mDbHelper, Class<T> clazz);}
### 6.2 默认实现
public class DefaultDbSupport<T> implements IDbSupport<T> { private DbHelper mDbHelper; private Class<T> mClazz; private QueryCenter mQueryCenter; private SQLiteDatabase getWritableDatabase() { return mDbHelper.getWritableDatabase(); } private SQLiteDatabase getReadableDatabase() { return mDbHelper.getReadableDatabase(); } @Override public long insert(T obj) { ContentValues values = contentValuesByTFinal(obj); SQLiteDatabase database = getWritableDatabase(); long insert = database.insert(DbUtil.getTableName(obj.getClass()), null, values); return insert; } public void insert(List<T> list) { getWritableDatabase().beginTransaction(); for (T obj : list) { insert(obj); } getWritableDatabase().setTransactionSuccessful(); getWritableDatabase().endTransaction(); } @Override public int delete(String whereClause, String... whereArgs) { SQLiteDatabase database = getWritableDatabase(); int delete = database.delete(DbUtil.getTableName(mClazz), whereClause, whereArgs); return delete; } @Override public int update(T obj, String whereClause, String... whereArgs) { SQLiteDatabase database = getWritableDatabase(); ContentValues values = contentValuesByTFinal(obj); int update = database.update(DbUtil.getTableName(obj.getClass()), values, whereClause, whereArgs); return update; } private static final Object[] sPutMethodArgs = new Object[2]; // 缓存,避免不必要的创建 private static final Map<String, Method> sPutMethod = new ArrayMap<String, Method>(); //缓存方法 private <T> ContentValues contentValuesByTFinal(T obj) { ContentValues values = new ContentValues(); Field[] fields = obj.getClass().getDeclaredFields(); try { for (Field field : fields) { field.setAccessible(true); String name = field.getName(); //name = $change, type = IncrementalChange if (field.isSynthetic() || "serialVersionUID".equals(name)) { continue; } Object value = field.get(obj); sPutMethodArgs[0] = name; sPutMethodArgs[1] = value; String typeName = field.getType().getSimpleName(); Method putMethod = sPutMethod.get(typeName); if (putMethod == null) { putMethod = ContentValues.class.getDeclaredMethod("put", String.class, value.getClass()); sPutMethod.put(typeName, putMethod); } //通过反射,执行方法,就像:values.put(name, value); putMethod.invoke(values, sPutMethodArgs); } } catch (Exception e) { e.printStackTrace(); } finally { sPutMethodArgs[0] = null; sPutMethodArgs[1] = null; } return values; } @Override public QueryCenter getQuery(Class<?> clazz) { if (mQueryCenter == null) { mQueryCenter = new QueryCenter(getReadableDatabase(), clazz); } return mQueryCenter; } public void setDbHelperAndClazz(DbHelper dbHelper, Class<T> clazz) { this.mDbHelper = dbHelper; this.mClazz = clazz; }}
### 6.3 统一入口
public class DbSupportFactory<T> { private static DbSupportFactory sDbSupportFactory; private DbHelper mDbHelper; private DbSupportFactory(Context context, T...obj) { mDbHelper = new DbHelper(context, obj); } public static <T> DbSupportFactory getFactory(Context context, T...obj) { if (sDbSupportFactory == null) { synchronized (DbSupportFactory.class) { if (sDbSupportFactory == null) { sDbSupportFactory = new DbSupportFactory(context); } } } return sDbSupportFactory; } public <T> IDbSupport getDbSupport(IDbSupport dbSupport, Class<T> clazz) { dbSupport.setDbHelperAndClazz(mDbHelper, clazz); return dbSupport; }}
### 6.4 使用
dbSupport = DbSupportFactory.getFactory(this, Person.class, Student.class).getDbSupport(new DefaultDbSupport(), Person.class);
“`
比如,现在App中有两张表,Person、Student,我们在获取工厂时,传入相关的class(用于创建表),然后在获取具体DbSupport时,传入要操作的那个表即可。
后记
本来是想在之前的笔记上改一下,分享出来,没想到,加上代码测试,整了一天,才算理清思路,这不是数据库封装的终点,恰好只是抛砖引玉一个开始。更深入的:如何存到SD卡中,我们操作数据库时,用的SQLiteDatabase是什么?当然,摆在眼前,最关键的是,如果像我文中提到的,更换其他数据库框架真的那么容易吗?
如何获取代码?
git clone https://github.com/droid4j/anKataLite.git
本篇对应的标签v0.3
git checkout v0.3
- android_sqlite数据库从基础到SDK封装
- Android_SQLite数据库详解
- 从头学android_SQLite数据库
- Android_SQLite数据库存储
- android_SQLite
- Android_sqlite数据库界面展示_141109
- Android动画SDK从基础到精通(AnimationForAndroid)
- Java开发系列--JDBC(从基础到封装)
- Android数据库进阶-从SQLite到ORMLite封装
- 从DirectX SDK升级到Windows SDK
- javaoop从 封装到继承
- mybatis从零基础到增删改查数据库
- 《大话Java:从零基础到数据库,Web开发》读书笔记
- 从变量到封装:一文带你为机器学习打下坚实的Python基础
- 把从数据库中查询出的一个字段封装到一个List中,返回List
- 将数据库的操作代码从servlet中剥离,封装到DAO中
- Java从数据库查询数据并封装到EXCEL表格中步骤
- Mybatis 基础应用-封装数据库
- JavaWeb之Servlet编程
- 使用OpenGL绘制六角星
- CPP_Basic_Summary_0.5
- Android Camera 实现滤镜方法
- [JVM]类加载机制
- android_sqlite数据库从基础到SDK封装
- 从dig命令理解DNS
- 回炉重造,第八话:php页面相互调用
- 用机器学习制作超级马里的关卡
- C++中关于函数的extern链接性以及extern关键字
- Android ViewPager使用方法小结
- 省赛选拔
- 升级xcode8之后,输入代码xcode闪退,插件导致闪退,KSImageName导致的
- 矩阵乘法——strassen算法