原文出处:Delightful persistence on Android
在文章开始之前,引用一位我最喜欢的武术大师之一李小龙的一段话:
“在我开始学习武术之时,对我来说一拳就是一拳,一脚就是一脚。在我学习武术之后,一拳不再是一拳,一脚也不再是一脚。现在,当我真正了解了这门艺术之后,便又感觉到一拳仍仅仅是一拳,一脚也仅仅是一脚罢了。”
在我使用sqlite数据库的时候,这种情况也差不多发生在我身上,不管是使用比如ORMLite, DBFlow等那样的ORM库,还是使用其他的数据库如Realm,一个数据库,不管它怎么变,也仅仅是一个数据库。
现在GitHub 满是管理sqlite数据库的安卓库 。几乎所有的库都用了ORM技术。但是,跟李小龙一样,我回到了本质,而且没有一个库给了我想要的完全掌控查询和操作的自由。对于很多项目而言数据库只是一个简单的数据库,因此别为了缓存数据或者存储数据模型而用libraries加重你的项目。
但是用普通的SQLiteOpenHelper又有点无聊,所以我继续搜索完美的数据库library,直到我发现了Square的SqlDelight。虽然不能说它完全满足了我的需要,但是至少对我有很大帮助。
声明:写这篇文章的时候,SQLDelight的最新版本是0.3.0 (2016-04-26)。
就如他们在repositories README文件中所说的:
SQLDelight根据SQL CREATE TABLE 语句生成Java model。这些model提供了一个类型安全的API去读写数据库。它帮助你把SQL statement有条理的放在一起,并方便从java获得。
所有的SQL statement都存在.sq文件中。你可以轻易的把数据库的改变更新到CVS中。
可以生成帮助你创建与查询表的 schema models。
可以自由的使用普通SQLite的同时帮助你处理了程式化的代码。
帮助你从Cursor映射到自定义model
支持Cursor 和 ContentValues一样的类型,但是添加了自定义类型甚至ENUM的支持
首先向build.gradle的buildscript方法中添加依赖。最新的版本可以在Square的sonatype找到:
buildscript { repositories { mavenCentral() } dependencies { classpath 'com.squareup.sqldelight:gradle-plugin:0.2.2' } }
上面的步骤已经足够让你开始使用SQLDelight了,但是Square那帮人还制作了一个支持语法与高亮的IntelliJ插件,推荐安装,它可以帮助你避免一些语法错误。
打开 Android Studio -> Preferences -> Plugins -> 搜索与安装 SQLDelight。
对于这篇文章,我做了一个基本的实现,你可以从 GitHub克隆。
Our basic database model Tables that we will create ourselves with the help of SQLDelight. | ![]() |
sample project中把com.alexsimo.delightfulpersistence作为root package,在src/main的根目录下面你需要创建一个sqlidelight文件夹,并拷贝你的root package 结构。
最终你得到一个如下的目录结构:
├── java │ └── com │ └── alexsimo │ └── delightfulpersistence │ ├── DelightfulApplication.java │ └── database │ ├── DatabaseManager.java │ ├── DelightfulOpenHelper.java │ ├── adapter │ │ └── DateAdapter.java │ └── model │ ├── Author.java │ └── Book.java └── sqldelight └── com └── alexsimo └── delightfulpersistence └── database └── model ├── Author.sq ├── Book.sq └── BookAuthor.sq
在sqldelight下面的model目录是你存放.sq文件的地方,它包含了app需要的sql语句。
你还能看到其它的文件,我稍后解释。现在我们先关注为model创建.sq文件。为了保持文章的简短,我只贴出创建和查询Author 和 Book 表的SQL语句,完整的语句可以在GitHub repository上找到。
Author:
CREATE TABLE author ( _id LONG PRIMARY KEY AUTOINCREMENT, name STRING NOT NULL, birth_year CLASS('java.util.Calendar') ); select_all: select * from author; select_by_name: select * from author where author.name = ?;
Book:
CREATE TABLE book ( _id LONG NOT NULL PRIMARY KEY AUTOINCREMENT, isbn STRING NOT NULL, title STRING NOT NULL, release_year CLASS('java.util.Calendar') ); select_all: select * from book; select_by_title: select * from book where book.title = ?; select_by_isbn: select * from book where book.isbn = ?;
The good thing about SQLDelight is that you also store your queries on .sq
files and will help you replace the bindable parameters as where book.isbn = ?
.
现在我们可以在我们的数据库中创建我们的表了,只需编译你的工程然后SQLDelight就会生成BookModel和AuthorModel。
如果查看BookModel的内部你可以看到它建立了CREATE TABLE SQL statement,而成员属性则代表表的每一列:
public interface BookModel { String TABLE_NAME = "book"; String _ID = "_id"; String ISBN = "isbn"; String TITLE = "title"; String RELEASE_YEAR = "release_year"; String CREATE_TABLE = "" + "CREATE TABLE book (\n" + " _id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n" + " isbn TEXT NOT NULL,\n" + " title TEXT NOT NULL,\n" + " release_year BLOB\n" + ")"; // 下面继续
除了创建表的语句,还生成了我们在.sq文件中存储的查询语句。
String SELECT_ALL = "" + "select *\n" + "from book"; String SELECT_BY_TITLE = "" + "select *\n" + "from book\n" + "where book.title = ?"; String SELECT_BY_ISBN = "" + "select *\n" + "from book\n" + "where book.isbn = ?";
注:其实它还生成了更多的东西,下面列出了一些。
有了这些SQL语句,我们可以轻易的创建表了,继承SQLiteOpenHelper:
public class DelightfulOpenHelper extends SQLiteOpenHelper { public static final String DB_NAME = "delightful.db"; public static final int DB_VERSION = 1; private static DelightfulOpenHelper instance; public static DelightfulOpenHelper getInstance(Context context) { if (null == instance) { instance = new DelightfulOpenHelper(context); } return instance; } private DelightfulOpenHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(BookModel.CREATE_TABLE); db.execSQL(AuthorModel.CREATE_TABLE); db.execSQL(BookAuthorModel.CREATE_TABLE); populate(db); } private void populate(SQLiteDatabase db) { AuthorPopulator.populate(db); BookPopulator.populate(db); } }
如果你使用Android官网上所说的SQLiteOpenHelper的基本实现,你可能会遇到一些并发的问题,就如Dmytro的博文中说明的,如果你是一个西班牙读者,看这篇文章.。
为了处理上面提到的问题,我创建了一个SQLiteHelper的wrapper:
DatabaseManager.java:
public class DatabaseManager { private static AtomicInteger openCount = new AtomicInteger(); private static DatabaseManager instance; private static DelightfulOpenHelper openHelper; private static SQLiteDatabase database; public static synchronized DatabaseManager getInstance() { if (null == instance) { throw new IllegalStateException(DatabaseManager.class.getSimpleName() + " is not initialized, call initialize(..) method first."); } return instance; } public static synchronized void initialize(DelightfulOpenHelper helper) { if (null == instance) { instance = new DatabaseManager(); } openHelper = helper; } public synchronized SQLiteDatabase openDatabase() { if (openCount.incrementAndGet() == 1) { database = openHelper.getWritableDatabase(); } return database; } public synchronized void closeDatabase() { if (openCount.decrementAndGet() == 0) { database.close(); } } }
上面的这个类,我在Application类中实例化它,或者使用Dagger。
使用一个非常简单的android connected test(需要设备或者模拟器),我们就能检测表是否成功创建:
public class DatabaseShould extends CustomRunner { @Override @Before public void setUp() throws Exception { super.setUp(); DbCommon.deleteDatabase(context); } @Test public void be_able_to_open_writable_database() throws Exception { SQLiteDatabase db = givenWritableDatabase(); assertTrue(db.isOpen()); assertTrue(!db.isReadOnly()); } @Test public void have_created_tables() throws Exception { SQLiteDatabase db = givenWritableDatabase(); HashSet<String> tables = givenAllTables(); Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'", null); cursor.moveToFirst(); do { String table = cursor.getString(0); tables.remove(table); } while (cursor.moveToNext()); cursor.close(); assertTrue(tables.isEmpty()); } private SQLiteDatabase givenWritableDatabase() { return DbCommon.givenWritableDatabase(context); } private HashSet<String> givenAllTables() { HashSet<String> tables = new HashSet<>(); tables.add(BookModel.TABLE_NAME); tables.add(AuthorModel.TABLE_NAME); tables.add(BookAuthorModel.TABLE_NAME); return tables; } }
同样你还可以像上面那样测试表的列的创建过程,这不是很好的范例,我很高兴你能提出一些改进:
public class AuthorTableShould extends CustomRunner { @Before public void setUp() throws Exception { super.setUp(); DbCommon.deleteDatabase(context); } @Test public void have_created_all_columns() throws Exception { HashSet<String> columns = givenAuthorColumns(); SQLiteDatabase db = givenWritableDatabase(); Cursor cursor = db.rawQuery("PRAGMA table_info(" + AuthorModel.TABLE_NAME + ")", null); cursor.moveToFirst(); int columnNameIndex = cursor.getColumnIndex("name"); do { String columnName = cursor.getString(columnNameIndex); columns.remove(columnName); } while (cursor.moveToNext()); assertTrue(columns.isEmpty()); cursor.close(); db.close(); } private SQLiteDatabase givenWritableDatabase() { return DbCommon.givenWritableDatabase(context); } private HashSet<String> givenAuthorColumns() { HashSet<String> columns = new HashSet<>(); columns.add(AuthorModel._ID); columns.add(AuthorModel.NAME); columns.add(AuthorModel.BIRTH_YEAR); return columns; } }
记住在.sq文件的CREATE TABLE定义中,我们使用了一个自定类型,java.util.Calendar。因为它不是一个原生的SQLite类型,我们必须为它创建一个adapter。
那么我们就来创建它,在我的sample project里,我把它叫做DateAdapter:
public class DateAdapter implements ColumnAdapter<Calendar> { @Override public Calendar map(Cursor cursor, int columnIndex) { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(cursor.getLong(columnIndex)); return calendar; } @Override public void marshal(ContentValues values, String key, Calendar value) { values.put(key, value.getTimeInMillis()); } }
这本该就完成了,但是我们还必须告诉SQLDelight如何使用它,所以找到并实现SQLDelight生成的BookModel和AuthorModel,重写里面的Author 和 Book Marshall类并传递这个calendar adapter。
@AutoValue public abstract class Author implements AuthorModel { private final static DateAdapter DATE_ADAPTER = new DateAdapter(); public final static Mapper<Author> MAPPER = new Mapper<>((Mapper.Creator<Author>) AutoValue_Author::new, DATE_ADAPTER); public static final class Marshal extends AuthorMarshal<Marshal> { public Marshal() { super(DATE_ADAPTER); } } }
我们暂时跳过MAPPER 变量,在讲到从表请求数据的时候将解释。注意这里的final类Marshal,就是在这里告诉SQLDelight,对于java.util.Calendar类型它应该使用你的adapter。在sample中我使用了谷歌的 @AutoValue 和 RetroLambda来避免一些重复的代码。
对于插入数据,我创建了一个叫做BookPopullator或者AuthorPopullator的类:
public class AuthorPopullator { public static void populate(SQLiteDatabase db) { db.insert(AuthorModel.TABLE_NAME, null, new Author.Marshal().name("J. K. Rowling") .birth_year(new GregorianCalendar(1965, 7, 31)) .asContentValues()); db.insert(AuthorModel.TABLE_NAME, null, new Author.Marshal().name("Bella Forests") .birth_year(new GregorianCalendar(197, 17, 31)) .asContentValues()); db.insert(AuthorModel.TABLE_NAME, null, new Author.Marshal().name("Norah Roberts") .birth_year(new GregorianCalendar(1950, 10, 10)) .asContentValues()); db.insert(AuthorModel.TABLE_NAME, null, new Author.Marshal().name("David Baldacci") .birth_year(new GregorianCalendar(1960, 8, 5)) .asContentValues()); db.insert(AuthorModel.TABLE_NAME, null, new Author.Marshal().name("Jeff Wheeler") .birth_year(new GregorianCalendar(1955, 13, 31)) .asContentValues()); } }
可以看到,SQLDelight为我们提供了一个非常流畅的builder,代码更干净了。
删除数据遵循相同的程序,你在.sq文件中定义sql删除语句然后使用SQLiteDatabase对象执行它。
为了演示如何使用SQLDelight查询数据,我像上面那样使用tests:
@Test public void be_able_to_return_cursor_with_all_default_authors() throws Exception { SQLiteDatabase db = givenWritableDatabase(); Cursor cursor = db.rawQuery(AuthorModel.SELECT_ALL, new String[0]); int AUTHOR_COUNT = 5; assertTrue(cursor.getCount() == AUTHOR_COUNT); } @Test public void map_cursor_with_domain_model() throws Exception { SQLiteDatabase db = givenWritableDatabase(); Cursor cursor = db.rawQuery(AuthorModel.SELECT_ALL, new String[0]); cursor.moveToFirst(); Author author = Author.MAPPER.map(cursor); assertNotNull(author); }
仔细看看Author.MAPPER.map(cursor)。对的!它能把cursor映射到model类中。对我来说这是非常棒的功能,因为它让我少些了很多程式化的代码,也更不易出错。
实际上,处理迁移并没有一个官方的方法,但是你可以查看一个 open issue来了解怎么回事。
在我的sample project中,我使用了一个比较不规范的方式把迁移语句存放到.sq文件中。
我把迁移文件存放在跟model创建与查询文件相同的目录中:
└── sqldelight └── com └── alexsimo └── delightfulpersistence └── database └── model ├── Author.sq ├── Book.sq ├── BookAuthor.sq ├── Migration_v1.sq └── Migration_v2.sq
如果你看一看 Migration_v1.sq 的内部,可以看到SQLDelight IntelliJ 插件与compiler要求你使用一个CREATE TABLE statement来开始一个.sq文件,因此我只是用并不会创建的dummy添加了一个CREATE TABLE语句:
CREATE TABLE dummy(_id LONG NOT NULL PRIMARY KEY AUTOINCREMENT); migrate_author: ALTER TABLE author ADD COLUMN country STRING NOT NULL;
然后在我们的自定义SQLiteOpenHelper类的onUpgrade()方法中我们就可以使用迁移语句了:
@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion < 2) { db.execSQL(Migration_v1Model.MIGRATE_AUTHOR); } if (oldVersion < 3) { db.execSQL(Migration_v2Model.MIGRATE_BOOK); } }
这并不是最干净的迁移方法,但是我相信Square的同学会在未来的SQLDelight版本中找到一个更好的方法。