標籤:

Android 坑檔案:Ormlite OrzCache...

每一個行業都有自己的坑,所謂經驗多寡無非是有沒有踩過坑。我把 Android 開發中遇到的各種坑彙集起來,於是就有了這部『Android 坑檔案』。

1、照例先講故事

話說,最近在做一個稍微有那麼一點兒邏輯的項目,需要用到資料庫,存兩張表,我們在這裡就叫它們 a 和 b。嗯,你以為我會自己寫 sql 然後 PrepareStatement? 哦不,那是不可能的,我這麼懶的人。

A.java

@DatabaseTablenclass A{n @DatabaseField(id = true)n public String id;n n @DatabaseField(foreign = true, foreignAutoRefresh = true)n public B b;n n @DatabaseFieldn public int status;n}n

B.java

@DatabaseTablenclass B{n @DatabaseField(id = true)n public String id;n n @ForeignCollectionFieldn public ForeignCollection<A> as;n n @DatabaseFieldn public int status;nn}n

那,上面就是表 a 和 b 對應的實體類,我們這些現代人,當然要用個 ORM 框架嘛,顯得自己也是用過框架的人~~

於是 Ormlite 登場了。其實我因為此前寫 UI 太多了,資料庫這塊兒的邏輯一直都是用 Afinaldb,很少有嘗試其他框架。所以你們懂的 T T。

我說你倒是趕緊說說你咋被坑了?別說『你懂的』,俺不懂。

別急別急,容我慢慢道來。

2、開啟 ObjectCache,想想都有點兒小激動呢

話說,A 和 B 的實例都有個屬性叫做 status,B 持有了好幾個 A,並且 B.status 取決於 A.status,還有這些 status 變化的非常頻繁。。為了把坑講清楚,容我再踩一下坑,來來來大家一起來。。

B{b1} --- (A{a1}, A{a2}, A{a3}, A{a4})

上面給出了一個 B 的實例,id=b1,同理後面的 a1-a4 分別表示 4 個 A 的實例的 id(下面我就用這些 id 來指代這些對象實例)。

考慮到 status 一直在變化,我們還要把 status 展示給 Ui,所以每次 status 改變都要通知 Ui 來獲取最新的數據。你以為我會讓 Ui 每次都去查資料庫嗎?我當然不想,可一旦對象落入 Ui 手裡,邏輯的改變並不見得就能同步到 Ui 那邊——這取決於擁有相同 id 的 A 的實例在邏輯層和 Ui 層是否共享引用,如果共享,那麼邏輯層做了修改後只需要告訴 Ui 數據更新了 ,你只需要刷新一下界面就好,不需要重新從資料庫取數據了;否則就每次都需要重新取數據。

於是,我覺得我應該做點兒什麼來保證所有具有相同 id 的 A 的實例或者 B 的實例在全局應該是同一個對象。我們都知道,用 Ormlite,我們會搞一個 DatabaseHelper 並從中獲取類的 Dao,比如:

DatabaseHelper.java

class DatabaseHelper{n ...n private volatile Dao<String, A> aDao;n private volatile Dao<String, B> bDao;n n public Dao<String, A> getADao(){n if(aDao == null){n synchronized(DatabaseHelper.class){n if(aDao == null){n aDao = getDao(A.class);n }n }n }n return aDao;n }n n ...nn}n

既然我們要保證 id 相同的對象是同一個對象,那麼使用自帶的 ObjectCache 就不能更合適了。於是我在 getADao 方法中添加了一句:

aDao.setObjectCache(true);n

這種情況,Ormlite 會使用默認的弱引用緩存。來,我們猜一下它的工作機制:

  • 對於某一個確定 id 的對象,一旦被查詢出來,且引用沒有被釋放,那麼它會一直被緩存著
  • 不管誰來查詢,都會先看看 Cached 中有沒有對應 id 的對象,有的話就直接返回。

哈,這不就是我想要的嘛。收拾行李回家歇著!

3、吾家有坑初長成

滿心歡喜的我打開了 『老友記』,哼著小曲兒,準備回去,走之前照例要編譯一把,提個代碼,不料運行後的 app 中 A 和 B 的實例不能及時更新狀態,導致邏輯混亂,真是奇哉怪也。

看了下代碼,更新狀態的邏輯都是對的嘛。還記得

B{b1} --- (A{a1}, A{a2}, A{a3}, A{a4})

a1 更新了狀態以後,會令 b1 的狀態發生改變,而我們知道全局 id 為 b1 的對象,只可能有一個,於是我們在其狀態發生變化之後馬上通知 Ui 刷新,可是 Ui 作為 app 的靈魂的窗口這時真可謂是瞎了其貓眼——沒有任何反應啊。

於是我就開始懷疑是不是前面『同一個對象』的功能有問題,各位看官,您猜怎麼著?Ui 持有的對象跟邏輯層的不!一!樣!

4、ObjectCache:我可以 Cache 你,也可以拒絕你

這可真是奇怪了。難道 Ormlite 在處理 外鍵對象的讀取和存儲時會繞過 ObjectCache?如果真是這樣,那豈不是出了 bug?!哇塞,這些更激動了~~

當然,別瞎激動,Ormlite 也是知名度很高的框架了,這麼低級的錯誤怎麼會犯呢。我在打了斷點跟蹤了一會兒,確定每次構建對象時,Ormlite 都會先到 ObjectCache 中查詢一番,沒找到才會構造新對象,並把構造出來的新對象存入 ObjectCache 中。

這一切看起來是如此的完美。下面是 ObjectCache 的介面,我們看到一切都跟我們猜測的沒有出入:

public interface ObjectCache {nn /**n * Register a class for use with this class. This will be called before any other method for the particular class isn * called.n */n public <T> void registerClass(Class<T> clazz);nn /**n * Lookup in the cache for an object of a certain class that has a certain id.n * n * @return The found object or null if none.n */n public <T, ID> T get(Class<T> clazz, ID id);nn /**n * Put an object in the cache that has a certain class and id.n */n public <T, ID> void put(Class<T> clazz, ID id, T data);nn /**n * Delete from the cache an object of a certain class that has a certain id.n */n public <T, ID> void remove(Class<T> clazz, ID id);nn /**n * Change the id in the cache for an object of a certain class from an old-id to a new-id.n */n public <T, ID> T updateId(Class<T> clazz, ID oldId, ID newId);nn /**n * Remove all entries from the cache of a certain class.n */n public <T> void clear(Class<T> clazz);nn /**n * Remove all entries from the cache of all classes.n */n public void clearAll();nn /**n * Return the number of elements in the cache.n */n public <T> int size(Class<T> clazz);nn /**n * Return the number of elements in all of the caches.n */n public int sizeAll();n}n

在看下我們實際用到的 ReferenceObjectCache,它實際上將對象存到了一個 ConcurrentHashMap 中,這個 Cache 可以不同類之間共享;useWeak 則用來標識是 WeakReference 還是 SoftReference,在這裡我們用的是前者。

public class ReferenceObjectCache implements ObjectCache {n private final ConcurrentHashMap<Class<?>, Map<Object, Reference<Object>>> classMaps =n new ConcurrentHashMap<Class<?>, Map<Object, Reference<Object>>>();n private final boolean useWeak;n n ...n}n

這就奇怪了,如果我去寫的話,也會這麼寫啊,而且每次查詢也確實都查了緩存的。

既然查過,卻又重新構建了對象,原因只可能有一個,那就是沒有存進去。

就在這時,我注意到了這段代碼:

public <T, ID> void put(Class<T> clazz, ID id, T data) {n Map<Object, Reference<Object>> objectMap = getMapForClass(clazz);n if (objectMap != null) {n if (useWeak) {n objectMap.put(id, new WeakReference<Object>(data));n } else {n objectMap.put(id, new SoftReference<Object>(data));n }n }n }n

如果 objectMap == null,這個方法直接就返回了,通常我們實現的時候,這裡的做法是構造一個實例賦值給 objectMap,同時放入 classMaps 當中。可它居然沒有這麼干。哦,看來這裡還有準入規定,如果我這個 Cache 當中沒有對應的 class 的 objectMap,對不起,不接待!

那麼問題來了,究竟在哪兒確定了接待的名單呢?稍稍跟蹤一下代碼,我們發現:

public synchronized <T> void registerClass(Class<T> clazz) {n Map<Object, Reference<Object>> objectMap = classMaps.get(clazz);n if (objectMap == null) {n objectMap = new ConcurrentHashMap<Object, Reference<Object>>();n classMaps.put(clazz, objectMap);n }n }n

而 register 方法是在 BaseDaoImpl.setObjectCache 處調用的,也就是我們在初始化 A 和 B 的 Dao 實例時調用的。

那麼,如果我程序剛剛啟動時,先調用 getADao 查詢 A,那麼這時候 bDao 還沒有被初始化,也就是說 ObjectCache 中並沒有註冊 B.class,所以。。這,就是問題所在。

可是 Ormlite 為什麼要這麼做呢?顯然作為一個 ObjectCache,保證同一個實體的查詢結果是同一個對象並不是它的本分,所謂 ObjectCache,只是在一定程度上減少 IO 次數進而提升性能的做法吧。

5、SingleReferenceObjectCache

啊,我們只需要稍微做一下修改,就可以讓這個 Cache 更加強大:

public class SingleReferenceObjectCache implements ObjectCache {n ...n public <T, ID> void put(Class<T> clazz, ID id, T data) {n Map<Object, Reference<Object>> objectMap = getMapForClass(clazz);n if (objectMap == null) {n registerClass(clazz);n }n objectMap = getMapForClass(clazz);n assert objectMap != null;n if (useWeak) {n objectMap.put(id, new WeakReference<Object>(data));n } else {n objectMap.put(id, new SoftReference<Object>(data));n }n }n ...n}n

6、如果 aDao 和 bDao 使用不同的 ObjectCache 對象

好,我們前面提到過 ObjectCache 的設計使它可以滿足不同類共享的需求,實際上我們前面使用默認的 ObjectCache 也是如此,這樣我們在用 aDao 去查 bDao 時,由於所有通過 bDao 可以查詢到的 B 的實例也都存在這個共享的 ObjectCache 當中,aDao 也毫無疑問的可以訪問到它們——而這一點,在保證所有查詢出來的同一實體即同一對象時非常關鍵。

那麼下面我們就做給它們分別設置一個 Cache 實例:

...naDao.setObjectCache(SingleReferenceObjectCache.makeWeakCache());n...nbDao.setObjectCache(SingleReferenceObjectCache.makeWeakCache());n...n

B{b1} --- (A{a1}, A{a2}, A{a3}, A{a4})

同樣是這個例子,我們在應用啟動後,

A a1 = DatabaseHelper.getInstance(context).getADao().queryForId("a1");nB b1 = a1.getB();nnB anotherB1 = DatabaseHelper.getInstance(context).getBDao().queryForId("b1");nnassert b1 != anotherB1;n

顯然,用我們修改過的 ObjectCache,不會像前面那樣出現 put 不進去的情況,那麼這時候b1 != anotherB1 很容易理解。不過接下來的事兒,你會怎麼想呢?

A a1 = DatabaseHelper.getInstance(context).getADao().queryForId("a1");nB b1 = a1.b;nnB anotherB1 = DatabaseHelper.getInstance(context).getBDao().queryForId("b1");nnassert b1 != anotherB1;nnfor(A a : anotherB1.as){n if(a.id.equals(a1.id)){n assert a == a1;n }n}n

也就是說,儘管用了不同的 Cache,在外鍵查詢到的對象中,還是會相同。

一會兒會相同,一會兒不相同,已經暈了。。。

好吧,這次我就不兜圈子,直接揭曉答案:

  • 注意到我們 getADao 時,bDao 還沒有被實例化
  • aDao 被初始化時,其實調用了 DaoManager.createDao,在這裡我們可以看到實際上 Ormlite 對所有的 Dao 實例是做了緩存的,lookupDao 和 registerDao 分別是查詢緩存和存入緩存。

    public synchronized static <D extends Dao<T, ?>, T> D createDao(ConnectionSource connectionSource, Class<T> clazz)n throws SQLException {nn ...nn ClassConnectionSource key = new ClassConnectionSource(connectionSource, clazz);n Dao<?, ?> dao = lookupDao(key);n if (dao != null) {n @SuppressWarnings("unchecked")n D castDao = (D) dao;n return castDao;n }nn ...nn registerDao(connectionSource, dao);n @SuppressWarnings("unchecked")n D castDao = (D) dao;n return castDao;n}n

  • 在 Dao 的 initialize 方法中,當前 Dao 會根據 對應的 Class 的註解來初始化 Dao,每個欄位都對應一個 FieldType 對象,FieldType.configDaoInformation 中會獲取到外鍵類對應的 Dao:

    public void configDaoInformation(ConnectionSource connectionSource, Class<?> parentClass) throws SQLException {n ...n Class<?> collectionClazz = (Class<?>) genericArguments[0];n DatabaseTableConfig<?> tableConfig = fieldConfig.getForeignTableConfig();n BaseDaoImpl<Object, Object> foundDao;n if (tableConfig == null) {n @SuppressWarnings("unchecked")n BaseDaoImpl<Object, Object> castDao =n (BaseDaoImpl<Object, Object>) DaoManager.createDao(connectionSource, collectionClazz);n foundDao = castDao;n } else {n @SuppressWarnings("unchecked")n BaseDaoImpl<Object, Object> castDao =n (BaseDaoImpl<Object, Object>) DaoManager.createDao(connectionSource, tableConfig);n foundDao = castDao;n }n foreignDao = foundDao;n foreignFieldType = findForeignFieldType(collectionClazz, parentClass, (BaseDaoImpl<?, ?>) foundDao);n foreignIdField = null;n foreignTableInfo = null;n mappedQueryForId = null;n ...n}n

知道了這些,我們再來看前面的 case :

// 下面這一句會初始化 A 對應的 Dao,並賦值給 aDaon// 同時因為外鍵,B 對應的 Dao 實例也會被初始化,並由 aDao 持有n// 這兩個 Dao 實例都將被緩存起來n// aDao 的 ObjectCache 打開nA a1 = DatabaseHelper.getInstance(context).getADao().queryForId("a1");nB b1 = a1.b;n// 截止到現在,a1 和 b1 都已經存入了 aDao.objectCache 當中nn// bDao 會被賦值為之前初始化好的 B 的 Dao 實例,並且打開 ObjectCachen// bDao 也會因為外鍵而持有 aDaonB anotherB1 = DatabaseHelper.getInstance(context).getBDao().queryForId("b1");nn// 由於 bDao 在查詢 B 時,只會訪問自己的 ObjectCache,所以 aDao 緩存的 b1 不會被查出來nassert b1 != anotherB1;nn// 同樣的,這時候 anotherB1 當中的 a1-a4 實際上是用 aDao 查詢的,當然也就可以拿到緩存的對象nfor(A a : anotherB1.as){n if(a.id.equals(a1.id)){n assert a == a1;n }n}n

注釋已經寫得很詳細了,簡單來說出現這個問題,原因主要在於 bDao 所引用的實例在 aDao 初始化時就已經初始化,而 bDao 的 ObjectCache 卻只有在 getBDao 時才能夠打開。除此之外,由於 aDao 緩存的 b1 實際上以後無論如何都不會被查詢到,所以現在你知道 Ormlite 為什麼在 ReferenceObjectCache.put 方法中,如果 objectMap == null,那麼就直接 return 了嗎?

7、小結

討論了這麼多,其實結論也是顯而易見的。我們的初衷是希望對於 id 相同的實體,Ormlite 查詢的結果使用保持是相同的實例,進而我們看到 Ormlite 本身的 ObjectCache 就可以實現這個功能,卻又引發了一系列的問題。簡單總結一下就是:

  • 對於某一個特定的類,Ormlite 全局只初始化一個 Dao 對應它,並用於資料庫操作,並且默認 ObjectCache 是關閉的。
  • 某一個特定類的 Dao 實例會持有外鍵對應的 Dao 實例。
  • 如果使用 ObjectCache,盡量同時顯式的初始化相應的 Dao,以確保 ObjectCache 能在 Dao 初始化時就可以立即打開。

本文作者:bennyhuo

轉載請註明出處:Android 坑檔案:Ormlite OrzCache...


推薦閱讀:

ORM是否必要?
項目里該不該用ORM?
在 PHP 領域裡,有哪些 ORM 比較好用?

TAG:Android | ORM |