美文网首页
GreenDao3.0 源码分析-Dao层

GreenDao3.0 源码分析-Dao层

作者: 孤独的追寻着 | 来源:发表于2018-05-13 11:23 被阅读0次

    GreenDao3.0系列文章:

    GreenDao3.0源码分析-Helper

    GreenDao3.0 源码分析-DaoMaster和DaoSeesion

    GreenDao3.0 源码分析-Dao层

    Dao 是GreenDao进行数据查询的一层,起到非常重要的作用,今晚我们就来聊聊GreenDao是如何做增删改查的吧。

    Order实体

    我们从稍微复杂的Order进行分析,去除自动生成的代码,源实体是:

    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());
    }</pre>
    

    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;</pre>
    

    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();
    }</pre>
    

    思路已经很清晰了,就通过获取多对象的Dao引用,进行查询操作,因为这里还添加时间的排序,所以添加增加了时间排序,一对一的关系也大致如此。

    Dao已经说完了,接下来我们来进行AbstractDao的解析

    AbstractDao

    AbstractDao封装了和数据库进行的增删改查功能。

    大致功能如下图:

    image

    AbstractDao提供了插入、更新、删除、保存,查询等功能,额为功能还包括对Rx1.0的适配,统计表的行数等。

    因为操作类似,我们这里只分析插入部分,其他部分可通过自己阅读完成理解:

    image

    可以看到上面的思维导图。子树是面对用户的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;
    }</pre>
    

    到这里 插入步骤如下:

    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();
                            }
                        }</pre>
    

    可以看到,多数据插入也只是遍历插入而已。

    插入后依然是更新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的优化,我们将单独抽出来说。

    相关文章

      网友评论

          本文标题:GreenDao3.0 源码分析-Dao层

          本文链接:https://www.haomeiwen.com/subject/nhtldftx.html