• Android 学习笔记之AndBase框架学习(五) 数据库ORM..注解,数据库对象映射...


    PS:好久没写博客了...

    学习内容:

    1.DAO介绍,通用DAO的简单调度过程..

    2.数据库映射关系...

    3.使用泛型+反射+注解封装通用DAO..

    4.使用AndBase框架实现对DAO的调用实现数据库基本操作..

    1.DAO..

      DAO..这个说法大家都不陌生..数据库操作访问对象的抽象接口层..在DAO内部封装好通用的方法..然后再使用一个具体类来进行实现..在我们想要调用这些方法对数据库进行相关操作时..只需要传递数据实体对象,就可以通过实体对象的传递从而实现对数据库进行操作..

      一个典型的DAO首先要有一个父类..这个父类是抽象出来的DAO接口层..内部封装了通用的抽象方法..一个简单的例子:

    package com.example.databaseactivity;
    
    import java.util.List;
    import java.util.Map;
    import android.database.sqlite.SQLiteOpenHelper;
    
    
    /*
     * 通用DAO父类..
     * */
    public interface DBDAO<T>{
    
        public SQLiteOpenHelper getDbHelper();
    
        /*
         * insert 函数..默认主键自增...
         * */
        public abstract long insert(T entity);
        
        /*
         * insert 函数..flag的值决定主键是否自增...
         * */
        public abstract long insert(T entity,boolean flag);
        
        /*
         * 插入实体类列表,默认主键自增..
         * */
        public abstract long insertList(List<T>entityList);
        
        /*
         * 插入实体类列表,flag决定主键是否自增..
         * */
        public abstract long insertList(List<T>entityList,boolean flag);
        
        /*
         * 根据id删除数据..
         * */
        public abstract long delete(int id);
        
        /* 
         * 根据id删除多个数据..
         * */
        public abstract long delete(Integer...ids);
        
        /*
         * 根据指定条件删除数据..
         * */
        public abstract long delete(String whereCaluse,String[] whereArgs);
        
        /*
         * 删除所有数据..
         * */
        public abstract long deleteAll();
        
        /*
         * 更新数据..
         * */
        public abstract long update(T entity);
        
        /*
         * 根据数据列表更新数据..
         * */
        public abstract long updateList(List<T> entityList);
        
        /*
         * 根据ID获取一行数据..
         * */
        public abstract T queryOne(int id);
        
        /*
         * 执行SQL查询语句..
         * class<T> 返回对象类型..
         * */
        public abstract List<T> rawQuery(String sql,String[] selectionArgs,Class<T> clazz);
        
        /*
         * 查询列表..
         * */
        public abstract List<T> queryList();
        
        /*
         * 根据多种限制条件进行查询.. 
         * */
        public abstract List<T> queryList(String[] columns,String selections,String[] selectionArgs,String groupBy,String having,String orderby,String limit);
    
        /*
         * 根据where条件进行查询..
         * */
        public abstract List<T> queryList(String selection,String selectionArgs);
        
        /*
         * 检查数据是否存在..
         * */
        public abstract boolean isExist(String sql,String[] selectionArgs);
        
        /*
         * 根据条件执行查询结果..将数据封装在map中..
         * */
        public List<Map<String, String>>queryMapList(String sql,String selectionArgs);
        
        /*
         * 返回查询结果条数..
         * */
        public int queryCount(String sql,String[] selectionArgs);
        
        /*
         * 封装执行sql代码..
         * */
        public void execSql(String sql, Object[] selectionArgs);
    }

      这就是一个DAO父类..内部提供了众多的抽象方法..那么有了抽象的方法..自然要有实现的子类..并且这个抽象的DAO接口层是不提供类型的..使用泛型的方式,那么为什么直接使用泛型呢?为什么不直接定义一个接口,后面还需要加上泛型..其实目的就是为了实现良好的扩展性..那么这个良好扩展的体现就是通过使用泛型来完成的...

      比如说我们把上面的类仅仅定义成一个抽象类..

    public interface DBDao{...封装了许多抽象函数...}

      那么只要我们想要进行数据库操作的时候,我们就需要implements这个接口...那么如果一个数据库中存在了多个表,那么就需要多个子类去implements这个抽象接口..那么这多个子类中的方法我们还是需要自己去人为去书写..这样就会导致每implements一次DBDao..我们都需要对内部的方法进行重写..这样显然是给我们自己去找麻烦..何必不再向上抽取呢?向上抽取目的就是为了减少这样的操作..避免一次次的重写..那么向上抽取就需要使用泛型了..

    public interface DBDao<T> {..抽闲函数..}

      我们同样使用一个泛型类进行实现..实现的过程需要使用到Java的注解和Java强大的反射机制..这里原理我们先不说..一会下面会有相关的解释..那么实现类就这样定义..

    public class AbDBDaoImpl<T> extends AbBasicDBDao implements AbDBDao<T>{..实现的过程..}

      这里实现的过程仍然以泛型的方式去实现...使用泛型的好处就是..无论我们传递的是什么类型..我们通过反射机制就能够获取传递来的实体类型从而去实例化对象..通过使用注解的方式我们就能够获取实体类型中的相关属性..有了实体对象以及实体对应的属性...那么就可以对数据表进行操作了...也就是说无论是一个表还是多个表的操作..我们只需要重写一次父类DAO的方法..在进行表操作的时候..我们只需要传递数据实体对象就可以了...或许还有点抽象..那么就来个实例..比如说上面的DBDAO<T>和DBDaoImpl<T>都已经写好了..那么我们就可以直接调用

    private UserInsideDao userDao = new UserInsideDao(MainActivity.this);//实例化对象..
    userDao.startReadableDatabase(false);//获取数据库..
    userDao.queryList(null,null,null,null,null,"create_time desc limit "+String.valueOf(pageSize)+" offset "+0,null);//直接可以执行查询函数..

      UserInsideDao其实就是一个服务层..使用它来传递我们想要初始化的数据表..以及获取数据库辅助类...这里的UserInsideDao继承了实现Dao抽象层的子层..那么UserInsideDao就可以去调用实现层的构造函数..将想初始化的表传递过去..那么这些表就会根据解析注解的方式对表进行相关的创建..

    public class UserInsideDao extends AbDBDaoImpl<LocalUser> {
        public UserInsideDao(Context context) {
            super(new DBInsideHelper(context),LocalUser.class);
        }
    }

      这里的DBInsideHelper才是真正对数据库进行操作的类...DBInsideHelper继承了AbDBHelper..获取数据库辅助类的同时..同时把LocalUser实体传递给AbDBDaoImpl..这就表示当前我需要对LocalUser表进行相关操作了..以下是DBInsideHelper的实现...

    public class DBInsideHelper extends AbDBHelper {
        // 数据库名
        private static final String DBNAME = "andbasedemo.db";
        
        // 当前数据库的版本
        private static final int DBVERSION = 1;
        // 要初始化的表
        private static final Class<?>[] clazz = { User.class,LocalUser.class,Stock.class,ChatMsg.class};
    
        public DBInsideHelper(Context context) {
            super(context, DBNAME, null, DBVERSION, clazz);
        }
    
    }

      总之最后会去调用AbDBDaoImpl中的构造函数..从而完成对数据表的一些初始化操作...然后就可以在主类中直接去调用相关函数来完成对数据库的基本操作了...

    2.数据库映射关系..

      数据库映射关系..说起来有点抽象..实际理解起来还是非常的简答的...数据库映射关系无非三种..一对一,一对多,多对多..这三种映射关系..映射关系其实就是实体和实体之间的实在关系..什么是一对一,一对多,多对多..拿一个简单的例子来说...

      比如说我们有三个数据表:

    学生表(学号,姓名,性别,年龄)    不难看出学生ID是主键..
    课程表(课程号,课程名称)       这里课程ID是主键..
    成绩表(学号,课程号,成绩)      这里的主键由学生ID和课程ID共同构成..

      一对一:比如说我们想要查询学生表ID为1,课程号ID为1的成绩..那么这行成绩只能对应学生表和课程表的一行数据..这就是一对一..也就是说表与表之间的数据相互对应只能是一行..

      一对多:查询学号为1的学生的成绩..这样在成绩表中会对应多条数据..但成绩表中的多条数据在学生表中只会对应一条..这个关系数据一对多..

      多对多:查询所有学生的成绩..那么数据表中多条数据对应多行...

      数据库映射关系如果真的理解过来还是比较容易的..千万不要理解反了..就拿查询一个班级里学号为1的学生...这个映射关系是一对一的关系...千万不要理解成一对多的关系...不要以为经过了多个层次就表示是一对多..这样去理解反而理解错了..这个关系是实体与实体之间的一对一,一对多,多对多..而不是经过了几层...这里还是需要注意的...

    3.使用泛型+反射+注解书写通用DAO...

      其实通用DAO最难实现的部分是使用反射去解析注解..也就是AbDBDaoImpl中方法的书写..这部分还是比较费劲的...

      通用DAO就拿这个例子来说吧...方法就一个..我就不多列举了..玩过JavaEE的对这方面还是非常清楚的...Hibernate DAO和SpringJDBC DAO都是以这种方式来对数据库进行操作的..不过传递数据对象的时候..类需要进行序列化操作..由于自己对SSH并不是很了解..就不在这班门弄斧了...还是说如何去实现的...

    public interface AbDBDao<T> {
        
         //获取数据库辅助类..
         public SQLiteOpenHelper getDbHelper();
         //查询数据,,以列表的形式进行展示..
         public abstract List<T> queryList(String[] columns, String selection,String[] selectionArgs, String groupBy, String having,String orderBy, String limit);     
    }    

      实现过程就需要AbDBDaoImpl类去实现了...

    public class AbDBDaoImpl<T> extends AbBasicDBDao implements AbDBDao<T> {
        
        /** The tag. */
        private String TAG = "AbDBDaoImpl";
        
        /** The db helper. */
        private SQLiteOpenHelper dbHelper;
        
        /**锁对象*/
        private final ReentrantLock lock = new ReentrantLock();
        
        /** The table name. */
        private String tableName;
        
        /** The id column. */
        private String idColumn;
        
        /** The clazz. */
        private Class<T> clazz;
        
        /** The all fields. */
        private List<Field> allFields;
        
        /** The Constant METHOD_INSERT. */
        private static final int METHOD_INSERT = 0;
        
        /** The Constant METHOD_UPDATE. */
        private static final int METHOD_UPDATE = 1;
    
        /** The Constant TYPE_NOT_INCREMENT. */
        private static final int TYPE_NOT_INCREMENT = 0;
        
        /** The Constant TYPE_INCREMENT. */
        private static final int TYPE_INCREMENT = 1;
        
        /**这个Dao的数据库对象*/
        private SQLiteDatabase db = null;
    
        /**
         * 用一个对象实体初始化这个数据库操作实现类.
         *
         * @param dbHelper 数据库操作实现类
         * @param clazz 映射对象实体
         */
        public AbDBDaoImpl(SQLiteOpenHelper dbHelper, Class<T> clazz) {
            this.dbHelper = dbHelper;
            if (clazz == null) {
                this.clazz = ((Class<T>) ((java.lang.reflect.ParameterizedType) super
                        .getClass().getGenericSuperclass())
                        .getActualTypeArguments()[0]);  //这里是获取传递过来的实体类型,比如说LocalUser.class...
            } else {
                this.clazz = clazz; //这里是直接获取...
            }
    
            if (this.clazz.isAnnotationPresent(Table.class)) { //如果存在声明的注解...这个注解数据表名属性..
                Table table = (Table) this.clazz.getAnnotation(Table.class);
                this.tableName = table.name(); //获取数据表名..
            }
    
            // 加载所有字段
            this.allFields = AbTableHelper.joinFields(this.clazz.getDeclaredFields(),
                    this.clazz.getSuperclass().getDeclaredFields());
    
            // 找到主键
            for (Field field : this.allFields) {
                if (field.isAnnotationPresent(Id.class)) {  //这里都是对注解的解析...这里的注解信息为ID...
                    Column column = (Column) field.getAnnotation(Column.class);
                    this.idColumn = column.name();
                    break;
                }
            }
    
            Log.d(TAG, "clazz:" + this.clazz + " tableName:" + this.tableName
                    + " idColumn:" + this.idColumn);
        }
    
        /**
         * 初始化这个数据库操作实现类.
         *
         * @param dbHelper 数据库操作实现类
         */
        public AbDBDaoImpl(SQLiteOpenHelper dbHelper) {
            this(dbHelper, null);
        }
    
        
        /**
         * 描述:TODO.
         *
         * @return the db helper
         * @see com.ab.db.orm.dao.AbDBDao#getDbHelper()
         */
        @Override
        public SQLiteOpenHelper getDbHelper() {
            return dbHelper;
        }
        @Override
        public List<T> queryList(String[] columns, String selection,
                String[] selectionArgs, String groupBy, String having,
                String orderBy, String limit) {
        
                List<T> list = new ArrayList<T>();
                Cursor cursor = null;
                try {
                    lock.lock();  //获取锁..避免多次操作导致问题发生...
                    Log.d(TAG, "[queryList] from"+this.tableName+" where "+selection+"("+selectionArgs+")"+" group by "+groupBy+" having "+having+" order by "+orderBy+" limit "+limit);
                    cursor = db.query(this.tableName, columns, selection,
                            selectionArgs, groupBy, having, orderBy, limit); //执行查询函数...
        
                    getListFromCursor(this.clazz,list, cursor);
                    
                    closeCursor(cursor);
                    
                    //获取关联域的操作类型和关系类型
                    String foreignKey = null;
                    String type = null;
                    String action = null;
                    //需要判断是否有关联表
                    for (Field relationsField : allFields) {
                        if (!relationsField.isAnnotationPresent(Relations.class)) {
                            continue;
                        }
                        
                        Relations relations = (Relations) relationsField.getAnnotation(Relations.class);
                        //获取外键列名
                        foreignKey = relations.foreignKey();
                        //关联类型
                        type = relations.type();
                        //操作类型
                        action = relations.action();
                        //设置可访问
                        relationsField.setAccessible(true);
                        
                        if(!(action.indexOf(ActionType.query)!=-1)){
                            return list;
                        }
                        
                        //得到关联表的表名查询
                        for(T entity:list){
                                
                                if(RelationsType.one2one.equals(type)){
                                    //一对一关系
                                    //获取这个实体的表名
                                    String relationsTableName = "";
                                    if (relationsField.getType().isAnnotationPresent(Table.class)) {
                                        Table table = (Table) relationsField.getType().getAnnotation(Table.class);
                                        relationsTableName = table.name();
                                    }
                                    
                                    List<T> relationsList = new ArrayList<T>();
                                    Field[] relationsEntityFields = relationsField.getType().getDeclaredFields();
                                    for (Field relationsEntityField : relationsEntityFields) {  //这里涉及到对主表进行再次遍历..这里主要原因是受到了外键的影响...
                                        Column relationsEntityColumn = (Column) relationsEntityField.getAnnotation(Column.class);
                                        //获取外键的值作为关联表的查询条件
                                        if (relationsEntityColumn.name().equals(foreignKey)) {
                                            
                                            //主表的用于关联表的foreignKey值
                                            String value = "-1";
                                            for (Field entityField : allFields) {
                                                //设置可访问
                                                entityField.setAccessible(true);
                                                Column entityForeignKeyColumn = (Column) entityField.getAnnotation(Column.class);
                                                if(entityForeignKeyColumn==null){
                                                    continue;
                                                }
                                                if (entityForeignKeyColumn.name().equals(foreignKey)) {
                                                    value = String.valueOf(entityField.get(entity));
                                                    break;
                                                }
                                            }
                                            //查询数据设置给这个域
                                            cursor = db.query(relationsTableName, null, foreignKey+" = ?",new String[]{value}, null, null, null, null);
                                            getListFromCursor(relationsField.getType(),relationsList, cursor);
                                            if(relationsList.size()>0){
                                                //获取关联表的对象设置值
                                                relationsField.set(entity, relationsList.get(0));
                                            }
                                            
                                            break;
                                        }
                                    }
                                    
                                }else if(RelationsType.one2many.equals(type) || RelationsType.many2many.equals(type)){
                                    //一对多关系
                                    
                                    //得到泛型里的class类型对象
                                    Class listEntityClazz = null;
                                    Class<?> fieldClass = relationsField.getType();
                                    if(fieldClass.isAssignableFrom(List.class)){
                                         Type fc = relationsField.getGenericType(); 
                                         if(fc == null) continue;  
                                         if(fc instanceof ParameterizedType) {
                                             ParameterizedType pt = (ParameterizedType) fc;  
                                             listEntityClazz = (Class)pt.getActualTypeArguments()[0];
                                         }
                                         
                                    }
                                    
                                    if(listEntityClazz==null){
                                        Log.e(TAG, "对象模型需要设置List的泛型");
                                        return null;
                                    }
                                    
                                    //得到表名
                                    String relationsTableName = "";
                                    if (listEntityClazz.isAnnotationPresent(Table.class)) {
                                        Table table = (Table) listEntityClazz.getAnnotation(Table.class);
                                        relationsTableName = table.name();
                                    }
                                    
                                    List<T> relationsList = new ArrayList<T>();
                                    Field[] relationsEntityFields = listEntityClazz.getDeclaredFields();
                                    for (Field relationsEntityField : relationsEntityFields) {
                                        Column relationsEntityColumn = (Column) relationsEntityField.getAnnotation(Column.class);
                                        //获取外键的值作为关联表的查询条件
                                        if (relationsEntityColumn.name().equals(foreignKey)) {
                                            
                                            //主表的用于关联表的foreignKey值
                                            String value = "-1";
                                            for (Field entityField : allFields) {
                                                //设置可访问
                                                entityField.setAccessible(true);
                                                Column entityForeignKeyColumn = (Column) entityField.getAnnotation(Column.class);
                                                if (entityForeignKeyColumn.name().equals(foreignKey)) {
                                                    value = String.valueOf(entityField.get(entity));
                                                    break;
                                                }
                                            }
                                            //查询数据设置给这个域
                                            cursor = db.query(relationsTableName, null, foreignKey+" = ?",new String[]{value}, null, null, null, null);
                                            getListFromCursor(listEntityClazz,relationsList, cursor);
                                            if(relationsList.size()>0){
                                                //获取关联表的对象设置值
                                                relationsField.set(entity, relationsList);
                                            }
                                            
                                            break;
                                        }
                                    }
                                    
                                }
                        }
                    }
                    
                } catch (Exception e) {
                    Log.e(this.TAG, "[queryList] from DB Exception");
                    e.printStackTrace();
                } finally {
                    closeCursor(cursor);
                    lock.unlock();
                }
        
                return list;
        }
    }

      这就是实现的过程..主要是通过使用反射机制+解析注解的方式来实现的...这里涉及到了一个关联表的事情...就拿刚才的那三个表来说吧...我想要查询学生号为1,课程号为1的成绩..这样成绩表就涉及到了两个表...因此还需要对关联表进行相关的操作..因此需要对所有的关联表进行遍历..然后进行相关的操作..我们可以看到这里又涉及到了主表的再次遍历...原因取决于外键的影响...

      简单的说一下外键..我们知道成绩表中主键是学生ID和课程ID的组合...但是学生ID又在学生列表中是主键..因此学生ID在成绩表中被称为成绩表关于学生表的外键...同样课程ID是成绩表关于课程表的外键...因为成绩表中的主键是复合型的..因此需要对外键进行一一获取..外键的获取通过对主表的遍历...最后把所有获取的外键进行保存..用来进行联合查询..这也就是那个步骤的目的...

      这样就简单的书写了一个通用的DAO..虽然只有一个方法..其他方法的实现我们可以自己去重写...

    4.使用AndBase中提供的DAO来完成对数据库的一系列操作...

      在AndBase中...源码就是通过对上面的封装给我们直接提供了一个通用的DAO接口层...我们都不需要去进行实现..直接就可以完成方法的调度过程...如果想要重写更多的方法..请继承实现类AbDBDaoImpl,然后在子类中去书写更多的方法...

      private UserInsideDao userDao = null
      //初始化数据库操作实现类
      userDao = new UserInsideDao(DBInsideSampleActivity.this);
            
      //(1)获取数据库 
      userDao.startReadableDatabase(false);
      //(2)执行查询
      userList = userDao.queryList(null, null, null, null, null, "create_time desc limit "+String.valueOf(pageSize)+ " offset " +0, null);
      totalCount = userDao.queryCount(null, null);
      //(3)关闭数据库
      userDao.closeDatabase(false);

      使用AndBase直接操作数据库...非常的简单...UserInsideDao类..这里表示我要对LocalUser实体对象进行操作...也就是对LocalUser表进行操作..剩下所有函数的调用都是对这个表进行相关的操作...

    package com.andbase.demo.dao;
    
    import android.content.Context;
    
    import com.ab.db.orm.dao.AbDBDaoImpl;
    import com.andbase.db.DBInsideHelper;
    import com.andbase.demo.model.LocalUser;
    
    public class UserInsideDao extends AbDBDaoImpl<LocalUser> {
        public UserInsideDao(Context context) {
            super(new DBInsideHelper(context),LocalUser.class);
        }
    }

      DBInsideHelper类...获取数据库辅助类的过程...初始化所有的数据表...

    package com.andbase.db;
    
    import android.content.Context;
    
    import com.ab.db.orm.AbDBHelper;
    import com.andbase.demo.model.LocalUser;
    import com.andbase.demo.model.Stock;
    import com.andbase.friend.ChatMsg;
    import com.andbase.model.User;
    
    public class DBInsideHelper extends AbDBHelper {
        // 数据库名
        private static final String DBNAME = "andbasedemo.db";
        
        // 当前数据库的版本
        private static final int DBVERSION = 1;
        // 要初始化的表
        private static final Class<?>[] clazz = {LocalUser};
    
        public DBInsideHelper(Context context) {
            super(context, DBNAME, null, DBVERSION, clazz);
        }
    
    }

      最后贴一下LocalUser类...传递的Class仅仅是一个LocalUser表..一般会传递多个表的..传递多个表就需要定义多个实体类就行了...

    package com.andbase.demo.model;
    
    import java.util.List;
    
    import com.ab.db.orm.annotation.Column;
    import com.ab.db.orm.annotation.Id;
    import com.ab.db.orm.annotation.Relations;
    import com.ab.db.orm.annotation.Table;
    @Table(name = "local_user")
    public class LocalUser {
    
        // ID @Id主键,int类型,数据库建表时此字段会设为自增长
        @Id
        @Column(name = "_id")
        private int _id;
        
        @Column(name = "u_id")
        private String uId;
    
        // 登录用户名 length=20数据字段的长度是20
        @Column(name = "name", length = 20)
        private String name;
    
        // 用户密码
        @Column(name = "password")
        private String password;
    
        // 年龄一般是数值,用type = "INTEGER"规范一下.
        @Column(name = "age", type = "INTEGER")
        private int age;
    
        // 创建时间
        @Column(name = "create_time")
        private String createTime;
        
        // 包含实体的存储,指定外键
        @Relations(name="stock",type="one2one",foreignKey = "u_id",action="query_insert")
        private Stock stock;
    
        // 包含List的存储,指定外键
        @Relations(name="stocks",type="one2many",foreignKey = "u_id",action="query_insert")
        private List<Stock> stocks;
        
        // 有些字段您可能不希望保存到数据库中,不用@Column注释就不会映射到数据库.
        private String remark;
    
    
        public int get_id() {
            return _id;
        }
    
    
        public void set_id(int _id) {
            this._id = _id;
        }
    
    
        public String getuId() {
            return uId;
        }
    
    
        public void setuId(String uId) {
            this.uId = uId;
        }
    
    
        public String getName() {
            return name;
        }
    
    
        public void setName(String name) {
            this.name = name;
        }
    
    
        public String getPassword() {
            return password;
        }
    
    
        public void setPassword(String password) {
            this.password = password;
        }
    
    
        public int getAge() {
            return age;
        }
    
    
        public void setAge(int age) {
            this.age = age;
        }
    
    
        public String getCreateTime() {
            return createTime;
        }
    
    
        public void setCreateTime(String createTime) {
            this.createTime = createTime;
        }
    
    
        public List<Stock> getStocks() {
            return stocks;
        }
    
    
        public void setStocks(List<Stock> stocks) {
            this.stocks = stocks;
        }
    
    
        public String getRemark() {
            return remark;
        }
    
    
        public void setRemark(String remark) {
            this.remark = remark;
        }
    
    
        public Stock getStock() {
            return stock;
        }
    
    
        public void setStock(Stock stock) {
            this.stock = stock;
        }
    
    }
  • 相关阅读:
    NLPIR的语义分析系统
    [译] 12步轻松搞定python装饰器
    python实现爬取千万淘宝商品的方法_python_脚本之家
    Deep Learning(深度学习)学习笔记整理系列 | @Get社区
    那些年,曾经被我们误读的大数据
    值得关注的10个python语言博客
    淘宝的评论归纳是如何做到的?
    pycharm激活码
    Windows下配置Qt 5.8+opencv 3.1.0开发环境
    Ubuntu安装opencv3.1.0后配置环境变量
  • 原文地址:https://www.cnblogs.com/RGogoing/p/4953980.html
Copyright © 2020-2023  润新知