android 的哪个数据库可确保安全性和较小的数据库文件大小?

Which database for android ensures security and small database file size?

我正在构建一个离线词典应用程序。数据库文件 150,000 行。我使用过 Sqlite,但我需要保护我的数据库,所以我使用 SqlCipher 库进行加密。我加密后遇到的问题是读取数据的速度,加密后的SqlCipher文件无法压缩。数据库文件大小显着增加。
Sqlite (9Mb) -> SqlCipher (93MB).
我还尝试使用 Realm 数据库。读取速度快,安全性好。但是,数据库文件的大小也显着增加。
Sqlite (9Mb) -> 领域 (50MB)
有什么办法可以减少数据库的大小吗? android还有一个数据库可以克服以上缺点 (安全性、速度、数据库大小)?

您尝试使用 VACUUM 了吗?

The VACUUM command rebuilds the entire database. There are several reasons an application might do this:

  • Unless SQLite is running in "auto_vacuum=FULL" mode, when a large amount of data is deleted from the database file it leaves behind empty space, or "free" database pages. This means the database file might be larger than strictly necessary. Running VACUUM to rebuild the database reclaims this space and reduces the size of the database file.

  • Frequent inserts, updates, and deletes can cause the database file to become fragmented - where data for a single table or index is scattered around the database file. Running VACUUM ensures that each table and index is largely stored contiguously within the database file. In some cases, VACUUM may also reduce the number of partially filled pages in the database, reducing the size of the database file further...

The VACUUM command works by copying the contents of the database into a temporary database file and then overwriting the original with the contents of the temporary file. When overwriting the original, a rollback journal or write-ahead log WAL file is used just as it would be for any other database transaction. This means that when VACUUMing a database, as much as twice the size of the original database file is required in free disk space...

编辑

还有一点不要忘记使用正确的数据类型,例如 TEXT 字段比 INTEGER 字段需要更多 space。

您或许可以考虑实施自己的 encryption/decryption,然后仅对实际敏感数据进行部分加密。

例如,下面的演示代码使用了一个包含 280000 个定义(尽管是重复定义)的基本词典 (word/definition)。未加密时占用 20.9mB,加密时占用 36.6mB。

  • mydb 是未加密的版本
  • mydbenc加密版

  • 这里又是实际存储的数据,比如说较长的词定义会产生相当大的影响,具有相同数量的词和定义但定义明显更长(在如果增加 14 个定义之一(每个定义重复 20000 次),那么 en-encrypted 数据库的大小增加了 4mB,加密数据库也增加了大约 4mB。(演示使用较大的 DB)

因此,对于 150,000 行,您的数据库的加密大小约为 20Mb。

大小也会受到加密方法的影响。一般来说,加密方法越弱,开销越小,但安全系数越低。

为了克服搜索的问题,示例应用程序在启动时将数据解密为临时table。这确实需要不到一分钟的时间,这可能是 unacceptable.

  • 加密字符串的一部分不等同于自己加密的那部分,因此执行搜索时会出现问题。

示例代码由两个数据库助手组成,一个是用于比较的未加密版本,另一个是加密版本。两者都使用相同的 table 和 3 列 id(未加密),worddefinition后两者在加密版中是加密的

数据库、table 和名称列是通过 class 命名的 DBConstants 中的常量定义的,按照 :-

public class DBConstants {

    public static int FILTEROPTION_ANYWHERE = 0;
    public static int FILTEROPTION_MUSTMATCH = 1;
    public static int FILTEROPTION_STARTSWITH = 2;
    public static int FILTEROPTION_ENDSWITH = 4;

    public static final String DBName = "mydb";
    public static final int DBVERSION = 1;
    public static final String DECRYPTEXTENSION = "_decrypt";

    public static class MainTable {

        public static final String TBLNAME = "main";
        public static final String COL_ID = BaseColumns._ID;
        public static final String COl_WORD = "_word";
        public static final String COL_DEFINITION = "_definition";

        public static final String CRT_SQL = "CREATE TABLE IF NOT EXISTS " + TBLNAME +
                "(" +
                COL_ID + " INTEGER PRIMARY KEY," +
                COl_WORD + " TEXT," +
                COL_DEFINITION + " TEXT" +
                ")";
    }

    public static class DecrtyptedMainTable {
        public static final String TBLNAME = MainTable.TBLNAME + DECRYPTEXTENSION;
        public static final String CRT_SQL = "CREATE TEMP TABLE IF NOT EXISTS " + TBLNAME +
                "(" +
                MainTable.COL_ID + " INTEGER PRIMARY KEY," +
                MainTable.COl_WORD + " TEXT, " +
                MainTable.COL_DEFINITION + " TEXT " +
                ")";
        public static final String CRTIDX_SQL = "CREATE INDEX IF NOT EXISTS " +
                TBLNAME + "_index " +
                " ON " + TBLNAME +
                "(" + MainTable.COl_WORD + ")";
    }
}

A Word class 用于允许根据 :-

提取 Word 对象
public class Word {
    private long id;
    private String word;
    private String definition;

    public Word() {
        this.id = -1L;
    }

    public Word(String word, String definition) {
        this(-1L,word,definition);
    }

    public Word(Long id, String word, String definition) {
        this.id = id;
        this.word = word;
        this.definition = definition;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getWord() {
        return word;
    }

    public void setWord(String word) {
        this.word = word;
    }

    public String getDefinition() {
        return definition;
    }

    public void setDefinition(String definition) {
        this.definition = definition;
    }
}

non-encrypted 数据库的数据库助手 DBHelperStandard.java(存在纯粹用于比较目的)是:-

public class DBHelperStandard extends SQLiteOpenHelper {

    SQLiteDatabase db;

    public DBHelperStandard(Context context) {
        super(context, DBConstants.DBName, null, DBConstants.DBVERSION);
        db = this.getWritableDatabase();
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(DBConstants.MainTable.CRT_SQL);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }

    public long insertWord(String word, String definition) {
        ContentValues cv = new ContentValues();
        cv.put(DBConstants.MainTable.COl_WORD,word);
        cv.put(DBConstants.MainTable.COL_DEFINITION,definition);
        return db.insert(DBConstants.MainTable.TBLNAME,null,cv);
    }

    public long insertWord(Word word) {
        return insertWord(word.getWord(),word.getDefinition());
    }

    public int deleteWord(long id) {
        String whereclause = DBConstants.MainTable.COL_ID + "=?";
        String[] whereargs = new String[]{String.valueOf(id)};
        return db.delete(DBConstants.MainTable.TBLNAME,whereclause,whereargs);
    }

    public int deleteWord(Word word) {
        return deleteWord(word.getId());
    }

    public int updateWord(long id, String word, String defintion) {

        ContentValues cv = new ContentValues();
        if (word != null && word.length() > 0) {
            cv.put(DBConstants.MainTable.COl_WORD,word);
        }
        if (defintion != null && defintion.length() > 0) {
            cv.put(DBConstants.MainTable.COL_DEFINITION,defintion);
        }
        if (cv.size() < 1) return 0;
        String whereclause = DBConstants.MainTable.COL_ID + "=?";
        String[] whereargs = new String[]{String.valueOf(id)};
        return db.update(DBConstants.MainTable.TBLNAME,cv,whereclause,whereargs);
    }

    public int updateWord(Word word) {
        return updateWord(word.getId(),word.getWord(),word.getDefinition());
    }

    public List<Word> getWords(String wordfilter, int filterOption, Integer limit) {
        ArrayList<Word> rv = new ArrayList<>();
        String whereclause = DBConstants.MainTable.COl_WORD + " LIKE ?";
        StringBuilder sb = new StringBuilder();
        switch (filterOption) {
            case 0:
                sb.append("%").append(wordfilter).append("%");
                break;
            case 1:
                sb.append(wordfilter);
                break;
            case 2:
                sb.append(wordfilter).append("%");
                break;
            case 4:
                sb.append("%").append(wordfilter);
        }
        String[] whereargs = new String[]{sb.toString()};
        if (wordfilter == null) {
            whereclause = null;
            whereargs = null;
        }
        String limitclause = null;
        if (limit != null) {
            limitclause = String.valueOf(limit);
        }
        Cursor csr = db.query(
                DBConstants.MainTable.TBLNAME,
                null,
                whereclause,
                whereargs,
                null,
                null,
                DBConstants.MainTable.COl_WORD,
                limitclause
        );
        while (csr.moveToNext()) {
            rv.add(new Word(
                    csr.getLong(csr.getColumnIndex(DBConstants.MainTable.COL_ID)),
                    csr.getString(csr.getColumnIndex(DBConstants.MainTable.COl_WORD)),
                    csr.getString(csr.getColumnIndex(DBConstants.MainTable.COL_DEFINITION))
                    ));
        }
        return rv;
    }
}

加密数据库的数据库助手DBHelperEncrypted.java是:-

public class DBHelperEncrypted extends SQLiteOpenHelper {

    private String secretKey;
    private String ivpParemeter;

    SQLiteDatabase db;
    public DBHelperEncrypted(Context context, String secretKey, String ivpParamter) {
        super(context, DBConstants.DBName + DBConstants.DECRYPTEXTENSION, null, DBConstants.DBVERSION);
        this.secretKey = secretKey;
        this.ivpParemeter = ivpParamter;
        db = this.getWritableDatabase();
    }

    @Override
    public void onCreate(SQLiteDatabase db) { db.execSQL(DBConstants.MainTable.CRT_SQL); }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }

    public long insertWord(String word, String definition) {
        ContentValues cv = new ContentValues();
        cv.put(DBConstants.MainTable.COl_WORD,EncryptDecrypt.encrypt(word,secretKey,ivpParemeter));
        cv.put(DBConstants.MainTable.COL_DEFINITION,EncryptDecrypt.encrypt(definition,secretKey,ivpParemeter));
        return db.insert(DBConstants.MainTable.TBLNAME,null,cv);
    }

    public long insertWord(Word word) {
        return insertWord(word.getWord(),word.getDefinition());
    }

    public int deleteWord(long id) {
        String whereclause = DBConstants.MainTable.COL_ID + "=?";
        String[] whereargs = new String[]{String.valueOf(id)};
        return db.delete(DBConstants.MainTable.TBLNAME,whereclause,whereargs);
    }

    public int deleteWord(Word word) {
        return deleteWord(word.getId());
    }

    public int updateWord(long id, String word, String defintion) {

        ContentValues cv = new ContentValues();
        if (word != null && word.length() > 0) {
            cv.put(DBConstants.MainTable.COl_WORD,EncryptDecrypt.encrypt(word,secretKey,ivpParemeter));
        }
        if (defintion != null && defintion.length() > 0) {
            cv.put(DBConstants.MainTable.COL_DEFINITION,EncryptDecrypt.encrypt(defintion,secretKey,ivpParemeter));
        }
        if (cv.size() < 1) return 0;
        String whereclause = DBConstants.MainTable.COL_ID + "=?";
        String[] whereargs = new String[]{String.valueOf(id)};
        return db.update(DBConstants.MainTable.TBLNAME,cv,whereclause,whereargs);
    }

    public int updateWord(Word word) {
        return updateWord(word.getId(),word.getWord(),word.getDefinition());
    }

    public List<Word> getWords(String wordfilter, int filterOption, Integer limit) {
        ArrayList<Word> rv = new ArrayList<>();
        String whereclause = DBConstants.MainTable.COl_WORD + " LIKE ?";
        StringBuilder sb = new StringBuilder();
        switch (filterOption) {
            case 0:
                sb.append("%").append(wordfilter).append("%");
                break;
            case 1:
                sb.append(wordfilter);
                break;
            case 2:
                sb.append(wordfilter).append("%");
                break;
            case 4:
                sb.append("%").append(wordfilter);
        }
        String[] whereargs = new String[]{sb.toString()};
        String limitclause = null;
        if (limit != null) {
            limitclause = String.valueOf(limit);
        }
        Cursor csr = db.query(
                DBConstants.DecrtyptedMainTable.TBLNAME,
                null,
                whereclause,
                whereargs,
                null,
                null,
                DBConstants.MainTable.COl_WORD,
                limitclause
        );
        while (csr.moveToNext()) {
            rv.add(
                    new Word(
                            csr.getLong(csr.getColumnIndex(DBConstants.MainTable.COL_ID)),
                            csr.getString(csr.getColumnIndex(DBConstants.MainTable.COl_WORD)),
                            csr.getString(csr.getColumnIndex(DBConstants.MainTable.COL_DEFINITION))
                    )
            );
        }
        return rv;
    }

    public void buildDecrypted(boolean create_index) {
        db.execSQL(DBConstants.DecrtyptedMainTable.CRT_SQL);
        Cursor csr = db.query(DBConstants.MainTable.TBLNAME,null,null,null,null,null,null);
        ContentValues cv = new ContentValues();
        while (csr.moveToNext()) {
            cv.clear();
            cv.put(DBConstants.MainTable.COL_ID,csr.getLong(csr.getColumnIndex(DBConstants.MainTable.COL_ID)));
            cv.put(DBConstants.MainTable.COl_WORD,
                    EncryptDecrypt.decrypt(csr.getString(csr.getColumnIndex(DBConstants.MainTable.COl_WORD)),secretKey,ivpParemeter));
            cv.put(DBConstants.MainTable.COL_DEFINITION,
                    EncryptDecrypt.decrypt(csr.getString(csr.getColumnIndex(DBConstants.MainTable.COL_DEFINITION)),secretKey,ivpParemeter));
            db.insert(DBConstants.DecrtyptedMainTable.TBLNAME,null,cv);
        }
        csr.close();
        if (create_index) {
            db.execSQL(DBConstants.DecrtyptedMainTable.CRTIDX_SQL);
        }
    }
}
  • 主要区别在于(可能不会被使用,因为看起来数据库已发货)插入和更新方法对数据进行加密,并且还包含一个方法buildDecrypted 从加密的 table 创建一个临时的 table。临时 table 用于搜索和提取数据。

    • 是临时的table它将在数据库关闭时被删除。通常,您只会在应用程序完成时关闭数据库。

加密和解密由 class EncryotDecrypt 根据 EncryptDecrypt.java 处理:-

public class EncryptDecrypt {
    public static Cipher cipher;

    /**
     * Encryption, irrespective of the USER type, noting that this should
     * only be used in conjunction with an EncryptDecrypt instance created
     * using the 2nd/extended constructor
     *
     * @param toEncrypt     The string to be encrypted
     * @return              The encrypted data as a string
     */
    public static String encrypt(String toEncrypt, String secretKey, String ivParameterSpec) {
        byte[] encrypted;
        try {
            if (cipher == null) {
                cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            }
            if (secretKey.length() < 16) {
                secretKey = (secretKey + "                ").substring(0,16);
            }
            SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(),"AES/CBC/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE,secretKeySpec,new IvParameterSpec(ivParameterSpec.getBytes()));
            encrypted = cipher.doFinal(toEncrypt.getBytes());
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return Base64.encodeToString(encrypted,Base64.DEFAULT);
    }

    /**
     * Decrypt an encrypted string
     * @param toDecrypt     The encrypted string to be decrypted
     * @return              The decrypted string
     */
    public static String decrypt(String toDecrypt, String secretKey, String ivParameterSpec)  {
        byte[] decrypted;
        try {
            if (cipher == null) {
                cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            }
            if (secretKey.length() < 16) {
                secretKey = (secretKey + "                ").substring(0,16);
            }
            SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(),"AES/CBC/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE,secretKeySpec,new IvParameterSpec(ivParameterSpec.getBytes()));
            decrypted = cipher.doFinal(Base64.decode(toDecrypt,Base64.DEFAULT));
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return new String(decrypted);
    }
}
  • 请注意,无论如何我都不是专家,但我相信您可以相对轻松地实施 less/more 安全加密方法。

最后,将所有内容放在一起用于此演示,是 MainActivity.java :-

public class MainActivity extends AppCompatActivity {

    public static final String SK = "mysecretkey";
    public static final String SALT = "124567890ABCDEFG";
    DBHelperEncrypted mDBE;
    DBHelperStandard mDBS;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDBE = new DBHelperEncrypted(this,SK,SALT);
        mDBS = new DBHelperStandard(this);

        //Stage 1 - Build the demo databases
        ArrayList<Word> wordsanddefinitions = new ArrayList<>();
        for (int i=0; i < 20000; i ++) {
            wordsanddefinitions.add(new Word("Apple","Something that falls on peoples heads that causes them to discover gravity."));
            wordsanddefinitions.add(new Word("Bachelor","An unmarried man."));
            wordsanddefinitions.add(new Word("Bachelor","A person who has been awarded a bachelor's degree."));
            wordsanddefinitions.add(new Word("Bachelor","A fur seal, especially a young male, kept from the breeding grounds by the older males."));
            wordsanddefinitions.add(new Word("Cat","A small domesticated carnivore, Felis domestica or F. catus, bred in a number of varieties."));
            wordsanddefinitions.add(new Word("Dog","A domesticated canid, Canis familiaris, bred in many varieties."));
            wordsanddefinitions.add(new Word("Eddy","A current at variance with the main current in a stream of liquid or gas, especially one having a rotary or whirling motion."));
            wordsanddefinitions.add(new Word("Eddy","A small whirlpool."));
            wordsanddefinitions.add(new Word("Eddy","Any similar current, as of air, dust, or fog."));
            wordsanddefinitions.add(new Word("Eddy","A current or trend, as of opinion or events, running counter to the main current."));
            wordsanddefinitions.add(new Word("Orange","A colour bewteen Red and Yellow."));
            wordsanddefinitions.add(new Word("Orange","a globose, reddish-yellow, bitter or sweet, edible citrus fruit."));
            wordsanddefinitions.add(new Word("Orange","any white-flowered, evergreen citrus trees of the genus Citrus, bearing this fruit, " +
                    "as C. aurantium (bitter orange, Seville orange, or sour orange) " +
                    "and C. sinensis (sweet orange), cultivated in warm countries."));
            wordsanddefinitions.add(new Word("Orange","Any of several other citrus trees, as the trifoliate orange."));
        }
        Log.d("STAGE1","Starting to build the Standard (non-encrypted) DB with " + String.valueOf(wordsanddefinitions.size()) + " definitions");
        mDBS.getWritableDatabase().beginTransaction();
        for (Word w: wordsanddefinitions ) {
            mDBS.insertWord(w);
        }
        mDBS.getWritableDatabase().setTransactionSuccessful();
        mDBS.getWritableDatabase().endTransaction();

        Log.d("STAGE2","Starting to build the Encrypted DB with " + String.valueOf(wordsanddefinitions.size()) + " definitions");
        mDBE.getWritableDatabase().beginTransaction();
        for (Word w: wordsanddefinitions) {
            mDBE.insertWord(w);
        }

        // Decrypt the encrypted table as a TEMPORARY table
        Log.d("STAGE 3","Bulding the temporary unencrypted table");
        mDBE.buildDecrypted(true); // Build with index on word column
        mDBE.getWritableDatabase().setTransactionSuccessful();
        mDBE.getWritableDatabase().endTransaction();

        // Database now usable
        Log.d("STAGE4","Extracting data (all words that include ap in the word) from the Standard DB");
        List<Word> extracted_s = mDBS.getWords("ap",DBConstants.FILTEROPTION_ANYWHERE,10);
        for (Word w: extracted_s) {
            Log.d("WORD_STANDARD",w.getWord() + " " + w.getDefinition());
        }

        Log.d("STAGE5","Extracting data (all words that include ap in the word) from the Encrypted DB");
        List<Word> extracted_e = mDBE.getWords("ap",DBConstants.FILTEROPTION_ANYWHERE,10);
        for (Word w: extracted_e) {
            Log.d("WORD_ENCRYPTED",w.getWord() + " " + w.getDefinition());
        }

        Log.d("STAGE5","Extracting demo data from standard and from encrypted without decryption");
        Cursor csr = mDBE.getWritableDatabase().query(DBConstants.MainTable.TBLNAME,null,null,null,null,null,null,"10");
        DatabaseUtils.dumpCursor(csr);
        csr = mDBS.getWritableDatabase().query(DBConstants.MainTable.TBLNAME,null,null,null,null,null,null,"10");
        DatabaseUtils.dumpCursor(csr);
        mDBS.close();
        mDBE.close();
    }
}

这个:-

  1. (第 1 阶段)根据重复 20000 次的 14 个核心 Word 定义(即 280000 个对象)创建 Word 个对象的 ArrayList。
  2. (阶段 1)使用插入方法构建 non-encrypted 数据库。
  3. (第2阶段)对加密数据库使用插入方法(加密数据)。

  4. (Stage 3) 构建临时解密的 table 和索引(可以使用 mDBE.buildDecrypted(false) 跳过索引); (它似乎没有太大的影响,尤其是因为它是在插入之后构建的)).

  5. (第 4 阶段)使用 getWords 方法(过滤)从 non-encrypted 数据库中提取一些数据并将提取的数据写入日志.

  6. (第 5 阶段)使用 getWords 方法(过滤)从加密数据库(来自解密的临时 table)中提取一些数据,并且将提取的数据写入日志。

    • 第 4 阶段和第 5 阶段的输出应该匹配。
  7. 从加密数据库的持久化 table(即加密数据,而非解密数据)中提取前 10 行(为简洁起见显示 2 行)。

  8. 从 non-encrypted 数据库中提取前 10 行(仅显示 2 行)并将 Cursor 转储到日志中。

    • 7 和 8 的输出显示持久数据库中的内容。

结果

6-05 13:51:36.932  D/STAGE1: Starting to build the Standard (non-encrypted) DB with 280000 definitions
06-05 13:51:59.274 D/STAGE2: Starting to build the Encrypted DB with 280000 definitions
06-05 13:52:45.327 D/STAGE 3: Bulding the temporary unencrypted table
06-05 13:52:45.350 W/CursorWindow: Window is full: requested allocation 111 bytes, free space 98 bytes, window size 2097152 bytes
    .........
06-05 13:53:35.024 D/STAGE4: Extracting data (all words that include ap in the word) from the Standard DB
06-05 13:53:35.346 D/WORD_STANDARD: Apple Something that falls on peoples heads that causes them to discover gravity.
    ..........
06-05 13:53:35.346 D/STAGE5: Extracting data (all words that include ap in the word) from the Encrypted DB
06-05 13:53:35.346 D/WORD_ENCRYPTED: Apple Something that falls on peoples heads that causes them to discover gravity.
    ..........
06-05 13:53:35.347 D/STAGE5: Extracting demo data from standard and from encrypted without decryption
06-05 13:53:35.347 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@d05c965
06-05 13:53:35.347 I/System.out: 0 {
06-05 13:53:35.347 I/System.out:    _id=1
06-05 13:53:35.347 I/System.out:    _word=3mqQlZl55WNjeZhALFQU7w==
06-05 13:53:35.347 I/System.out:    _definition=s9Waa2HLUS2fy8q1uC9/MEKogmImu6m9MIpi9wasD9D3Zom6+/u40DnFfP6zXOyI8IgnQOKcWfQ8
06-05 13:53:35.347 I/System.out: G3uJN9a/YHMoQdEQMDMEEdSE2kWyJrc=
06-05 13:53:35.347 I/System.out: }
06-05 13:53:35.347 I/System.out: 1 {
06-05 13:53:35.347 I/System.out:    _id=2
06-05 13:53:35.347 I/System.out:    _word=LtLlycoBd9fm3eYF9aoItg==
06-05 13:53:35.347 I/System.out:    _definition=B1XJJm0eC8wPi3xGg4XgJtvIS3xL7bjixNhVAVq1UwQ=
06-05 13:53:35.347 I/System.out: }

06-05 13:53:35.348 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@7f1b63a
06-05 13:53:35.348 I/System.out: 0 {
06-05 13:53:35.348 I/System.out:    _id=1
06-05 13:53:35.348 I/System.out:    _word=Apple
06-05 13:53:35.348 I/System.out:    _definition=Something that falls on peoples heads that causes them to discover gravity.
06-05 13:53:35.348 I/System.out: }
06-05 13:53:35.348 I/System.out: 1 {
06-05 13:53:35.348 I/System.out:    _id=2
06-05 13:53:35.348 I/System.out:    _word=Bachelor
06-05 13:53:35.348 I/System.out:    _definition=An unmarried man.
06-05 13:53:35.348 I/System.out: }
06-05 13:53:35.349 I/System.out: <<<<<