1. 程式人生 > >GreenDao3.0 原始碼分析-Dao層

GreenDao3.0 原始碼分析-Dao層

GreenDao3.0系列文章:

  

  Dao 是GreenDao進行資料查詢的一層,起到非常重要的作用,今晚我們就來聊聊GreenDao是如何做增刪改查的吧。

    Order實體

        我們從稍微複雜的Order進行分析,去除自動生成的程式碼,源實體是:
        
@Entity(active = true, nameInDb = "ORDERS")
public class Order {
    @Id
    private Long id;
    private java.util.Date date;
    private long customerId;
    @ToOne(joinProperty = "customerId")
    private Customer customer;

}
        如上圖,Order和Customer物件是一對一的關係,下面我們來看看生成的OrderDao:    

    OrderDao

        以下幾點是我歸納的要點
        1、所有的實體DAO物件都繼承自 AbstractDao<Order, Long>。
        2、TABLENAME常量定義了資料庫表名,預設為實體類名大寫,nameInDb 可以自定義表名。
        3、每個實體Dao都有一個Properties來管理物件實體屬性對錶各列的對映關係,對像為Property。
        4、提供建立表和刪除表的實現。
        5、提供Statement繫結對應例項的方法。
        6、讀取Cursor轉化成物件。
        7、獲得主鍵。
        以上是最基礎的操作,是必須的,還有因為一些特殊的:
        8、當Java實體需要轉成其他型別,比如String儲存時,需要提供一個轉化器PropertyConverter
        9、為了查詢告訴,把實體Id設定成RowId
        10、還有就是一對多關係,建立的一些關聯性程式碼。
        上面歸納的就是實體Dao提供的功能,下面我們逐步對程式碼進行解析:
         1、2、3我就不說了,非常簡單,就是通過   Property屬性物件來進行管理,每一個Property就是物件資料庫的一列。
         3、4是資料庫的基本操作,通過Database資料庫物件執行Sql語句建立表和刪除表,每次建立刪除是通過DaoMaster進行操作的,兩個方法都是靜態方法。
         我們來簡單說下5:
private final Date2LongConver dateConverter = new Date2LongConver();
protected final void bindValues(SQLiteStatement stmt, Order entity) {
        stmt.clearBindings();
 
        Long id = entity.getId();
        if (id != null) {
            stmt.bindLong(1, id);
        }
 
        Date date = entity.getDate();
        if (date != null) {
            stmt.bindLong(2, dateConverter.convertToDatabaseValue(date));
        }
        stmt.bindLong(3, entity.getCustomerId());
    }
  SQLiteStatement是我們定義好的一些增刪改查語句的宣告,通過以?來做佔位符,達到提供效能的目的,這裡就是把Order例項的資料資訊,按照序號,進行繫結到SQLiteStatement中,以供資料庫做查詢等操作,從上面原始碼我們還能看到,GreenDao的轉化器,其實就是按照一定的規則,生成對映的Date2LongConver dateConverter = new Date2LongConver();物件,然後執行方法,達到轉換的母目的。    下面我們看看讀物件的操作:
      
 public Order readEntity(Cursor cursor, int offset) {
        Order entity = new Order( //
            cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0), // id
            cursor.isNull(offset + 1) ? null : dateConverter.convertToEntityProperty(cursor.getLong(offset + 1)), // date
            cursor.getLong(offset + 2) // customerId
        );
        return entity;
    }

讀的操作也是非常簡單,通過判空後進行賦值,相應的需要轉化的物件也會轉化。

 7、8、9各位看官自己檢視,比較簡單getKey(Order entity)獲取主鍵,updateKeyAfterInsert(Order entity, long rowId)是插入成功後更換Key,hasKey(Order entity)判斷是否有主鍵。

 下面我們說說,關於GreenDao是如何實現一對多的問題:

  我們再來引入一個類Customer,Customer類和Order是一對多的關係

 @ToMany(joinProperties = {
            @JoinProperty(name = "id", referencedName = "customerId")
    })
    @OrderBy("date ASC")
    private List<Order> orders;

GreenDao處理一對多的關係,是通過取得OrderDao的引用來進行查詢:

public List<Order> getOrders() {
        if (orders == null) {
            final DaoSession daoSession = this.daoSession;
            if (daoSession == null) {
                throw new DaoException("Entity is detached from DAO context");
            }
            OrderDao targetDao = daoSession.getOrderDao();
            List<Order> ordersNew = targetDao._queryCustomer_Orders(id);
            synchronized (this) {
                if (orders == null) {
                    orders = ordersNew;
                }
            }
        }
        return orders;
    }

id就是customerId,我們再看看_queryCustomer_Orders這裡方法:

  public List<Order> _queryCustomer_Orders(long customerId) {
        synchronized (this) {
            if (customer_OrdersQuery == null) {
                QueryBuilder<Order> queryBuilder = queryBuilder();
                queryBuilder.where(Properties.CustomerId.eq(null));
                queryBuilder.orderRaw("T.'DATE' ASC");
                customer_OrdersQuery = queryBuilder.build();
            }
        }
        Query<Order> query = customer_OrdersQuery.forCurrentThread();
        query.setParameter(0, customerId);
        return query.list();
    }

思路已經很清晰了,就通過獲取多物件的Dao引用,進行查詢操作,因為這裡還新增時間的排序,所以新增增加了時間排序,一對一的關係也大致如此。

Dao已經說完了,接下來我們來進行AbstractDao的解析

AbstractDao

    AbstractDao封裝了和資料庫進行的增刪改查功能。

    大致功能如下圖:


AbstractDao提供了插入、更新、刪除、儲存,查詢等功能,額為功能還包括對Rx1.0的適配,統計表的行數等。

因為操作類似,我們這裡只分析插入部分,其他部分可通過自己閱讀完成理解:


可以看到上面的思維導圖。子樹是面對使用者的API,最終單個實體插入會執行到insertInsideTx,而多資料實體會執行到executeInsertInTx。

這裡我來理一下思路,GreenDao不管是做查詢還是其他操作都是使用Statement來進行優化效能的,而且Statement是可以重用的,所以GreenDao有自己的Statement管理類,就是TableStatements,我們來看看TableStatements對插入宣告的建立:

 public DatabaseStatement getInsertStatement() {
        if (insertStatement == null) {
            String sql = SqlUtils.createSqlInsert("INSERT INTO ", tablename, allColumns);
            DatabaseStatement newInsertStatement = db.compileStatement(sql);
            synchronized (this) {
                if (insertStatement == null) {
                    insertStatement = newInsertStatement;
                }
            }
            if (insertStatement != newInsertStatement) {
                newInsertStatement.close();
            }
        }
        return insertStatement;
    }

從上面程式碼我們知道,TableStatements維護著一個insertStatement物件,如果不為null就直接返回,為null就拼接建立,以達到複用優化效能的作用,這是資料庫常見的操作,SqlUtils工具類是Sql語句拼接的工具,大家有興趣自己看一下。

我們來先聊聊單個實體做插入的時候,從面向使用者的API獲取到想用的插入,或者插入或替換的Statement後,插入操作會執行

executeInsert(T entity, DatabaseStatement stmt, boolean setKeyAndAttach)方法:

private long executeInsert(T entity, DatabaseStatement stmt, boolean setKeyAndAttach) {
        long rowId;
        //先判斷當前執行緒是否連線了資料庫
        if (db.isDbLockedByCurrentThread()) {
            //返回true 直接插入資料
            rowId = insertInsideTx(entity, stmt);
        } else {
            //在鎖定stmt之前通過開啟transation請求連線
            db.beginTransaction();
            try {
                rowId = insertInsideTx(entity, stmt);
                db.setTransactionSuccessful();
            } finally {
                db.endTransaction();
            }
        }
        if (setKeyAndAttach) {
            updateKeyAfterInsertAndAttach(entity, rowId, true);
        }
        return rowId;
    }

到這裡 插入步驟如下:

1、通過判斷當前執行緒是否和資料庫關聯來決定是否需要開啟事務。

2、最後都執行insertInsideTx,裡面的操作很簡單就是呼叫之前子類的bindValue方法進行繫結值後執行Sql操作。

3、插入後的善後處理,這裡就是更新實體的ID為RowId和做記憶體快取,還有一些特殊操作實體繫結DaoSeesion,使用active = true會用到。

我再來看看executeInsertInTx,獲取到Statement類似,因為是多資料插入,強制使用事務:

這裡我們再引入一個概念,就是IdentityScope<K, T>是GreenDao用來做記憶體快取的,可以看成是一個Map,如果是Long,GreenDAO做了對應的優化,因為多資料插入是比較耗時的,所以,我們執行插入之前需要加鎖,防止多執行緒的問題。

                     SQLiteStatement rawStmt = (SQLiteStatement) stmt.getRawStatement();
                        for (T entity : entities) {
                            bindValues(rawStmt, entity);
                            if (setPrimaryKey) {
                                //執行Sql語句 並返回物件的rowId
                                long rowId = rawStmt.executeInsert();
                                updateKeyAfterInsertAndAttach(entity, rowId, false);
                            } else {
                                rawStmt.execute();
                            }
                        }

可以看到,多資料插入也只是遍歷插入而已。

插入後依然是更新ID,然後就是記憶體快取,我們來看看下面這個方法:

protected final void attachEntity(K key, T entity, boolean lock) {
        attachEntity(entity);
        if (identityScope != null && key != null) {
            if (lock) {
                identityScope.put(key, entity);
            } else {
                identityScope.putNoLock(key, entity);
            }
        }
    }

可以看到identityScope 就是用來維護記憶體快取的鍵值對,通過判斷是否加鎖執行相應的put操作。

說到這裡,GreenDao是怎麼優化和做快取的大家應該都大致瞭解了吧:

1、通過Statement的複用,達到優化的效果,這是所有資料庫都通用的。

2、通過Key對映儲存到記憶體中,儲存的值當前是軟引用拉,要不很容易爆表。

其他操作型別大家可以花店心思去學一下。

還有就是GreenDao除了用弱引用外,在Key為Long時還特別做了Map的優化,我們將單獨抽出來說。