数据库
首页 > 数据库> > 红橙Darren视频笔记 数据库操作优化 数据查询(数据库操作)中

红橙Darren视频笔记 数据库操作优化 数据查询(数据库操作)中

作者:互联网

上一节仅仅是做到了有这个功能,这次我们对上一次的代码进行优化
主要有两个方面可以优化

1.利用数据库事务进行优化

我们作如下修改:
IDaoSupport增加批量插入接口

    // 批量插入数据
    public void inert(List<T> t);

在实现类中实现该方法 并使用数据库事务

    @Override
    public void inert(List<T> data) {
        mSqLiteDatabase.beginTransaction();
        for (T datum: data){
            inert(datum);
        }
        mSqLiteDatabase.setTransactionSuccessful();
        mSqLiteDatabase.endTransaction();
    }

对数据库插入测试方法稍作修改

        IDaoSupport<Person> daoSupport = DaoSupportFactory.getFactoryInstance(MainActivity.this).getDao(Person.class);
        // 最少的知识原则
        new Thread(() -> {
            int totalNum = 1000;
            List<Person> personList = new ArrayList<>();
            long startTime = System.currentTimeMillis();
            for (int i = 0; i < totalNum; i++) {
                personList.add(new Person("hjcai", i));
            }
            daoSupport.inert(personList);
            long endTime = System.currentTimeMillis();
            Log.e(TAG, " insert " + totalNum + " cost time -> " + (endTime - startTime));
        }).start();

以下是修改后执行的时间

2021-03-17 21:08:52.164 8977-9012/com.example.learneassyjoke E/MyActivity:  insert 1000 cost time -> 541
2021-03-17 21:09:02.490 9027-9052/com.example.learneassyjoke E/MyActivity:  insert 1000 cost time -> 394
2021-03-17 21:09:07.316 9063-9088/com.example.learneassyjoke E/MyActivity:  insert 1000 cost time -> 414

还记得我们上一节的操作时间都在5s以上
因此批量操作数据库时一定要使用数据库事务进行优化 数据库操作效率可以提高很多倍

参考链接 https://blog.csdn.net/hfsime/article/details/39502699
QUESTION: 为什么事务能够显著的提高数据库操作效率?
在整个数据库中,执行任何一类操作诸如sqliteDatabase.delete()都会创建一个数据库事务,存在这样一种影响效率的情况:需要一次删除N条数据,程序就会创建N次事务。而当自身使用显式的数据库事务时,这些删除数据的操作,就在一次数据库事务中执行,从而在删除(插入、更新)数据特别大的时候大大的提高效率。

2.减少反射的调用次数

在DaoSupport新增两个类变量

    private static final Object[] mPutMethodArgs = new Object[2];//存储key value 如 name "hjcai"//个人觉得这个变量意义不大
    // 数据库优化 缓存数据类型 减少反射的调用次数
    private static final Map<String, Method> mPutMethods = new ArrayMap<>();

对contentValuesByObj稍作修改 只有第一次插入类型T时才进行反射,否则可以从mPutMethodArgs取得该类的各个filed的put方法 这样在批量操作时大大降低了反射调用的次数

    private ContentValues contentValuesByObj(T obj) {
        ContentValues contentValues = new ContentValues();
        // 通过反射获取mClazz定义的filed(以Person为例 返回的是age 和 name字段)
        Field[] fields = mClazz.getDeclaredFields();
        for (Field field : fields) {
            Method putMethod;
            try {
                // 设置权限,私有和共有都可以访问
                field.setAccessible(true);
                // 获取field的名称(如age)
                String key = field.getName();
                // 获取field的value(如30)
                Object value = field.get(obj);

                mPutMethodArgs[0] = key;
                mPutMethodArgs[1] = value;

                // 虽然使用反射会有一点性能的影响 但是影响很小 而且源码里面  activity实例的创建 View创建反射等都使用了反射 因此这里也会使用反射 获取put方法
                String filedTypeName = field.getType().getName();//获取filed的数据类型(如Integer)
                // filedTypeName的作用是作为存储mPutMethods的key
                // 因此使用的key类似 java.lang.String int boolean等
                putMethod = mPutMethods.get(filedTypeName);
                if (putMethod == null) {
                    putMethod = ContentValues.class.getDeclaredMethod("put",
                            String.class, value.getClass());
                    // 缓存PutMethods 下次就不需要再反射了
                    mPutMethods.put(filedTypeName, putMethod);
                }
                // 通过反射执行ContentValues的putXXX方法
                // 相当于调用类似 contentValues.put("age",30);
                putMethod.invoke(contentValues, mPutMethodArgs);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                mPutMethodArgs[0] = null;
                mPutMethodArgs[1] = null;
            }
        }
        return contentValues;
    }

以下是测试结果

2021-03-18 19:35:27.379 11895-11920/com.example.learneassyjoke E/MyActivity:  insert 1000 cost time -> 411
2021-03-18 19:35:32.796 11930-11954/com.example.learneassyjoke E/MyActivity:  insert 1000 cost time -> 427
2021-03-18 19:35:38.003 11968-11993/com.example.learneassyjoke E/MyActivity:  insert 1000 cost time -> 402
2021-03-18 19:35:43.261 12002-12026/com.example.learneassyjoke E/MyActivity:  insert 1000 cost time -> 402
2021-03-18 19:35:48.241 12039-12063/com.example.learneassyjoke E/MyActivity:  insert 1000 cost time -> 392

相对于事务,这次好像没有多大提升,但是能优化的地方也优化一下吧。
PS:以上减少反射调用的思路来自于源码View的创建过程中的inflate部分
setContentView(R.layout.activity_main); ->
getDelegate().setContentView(layoutResID);->
LayoutInflater.from(mContext).inflate(resId, contentParent);
return inflate(resource, root, root != null);
return inflate(parser, root, attachToRoot);
rInflateChildren(parser, temp, attrs, true);
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
final View view = createViewFromTag(parent, name, context, attrs);
return createViewFromTag(parent, name, context, attrs, false);
在比较老版的API中createViewFromTag还是利用反射创建View的

	private final Object[] mConstructorArgs = new Object[2];
	private View createViewFromTag(Context context, String name, AttributeSet attrs) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue((String)null, "class");
        }

        View view;
        try {
            this.mConstructorArgs[0] = context;
            this.mConstructorArgs[1] = attrs;
            View var4;
            if (-1 == name.indexOf(46)) {
                for(int i = 0; i < sClassPrefixList.length; ++i) {
                    view = this.createViewByPrefix(context, name, sClassPrefixList[i]);
                    if (view != null) {
                        View var6 = view;
                        return var6;
                    }
                }

                var4 = null;
                return var4;
            }

            var4 = this.createViewByPrefix(context, name, (String)null);
            return var4;
        } catch (Exception var10) {
            view = null;
        } finally {
            this.mConstructorArgs[0] = null;
            this.mConstructorArgs[1] = null;
        }

        return view;
    }
	private static final SimpleArrayMap<String, Constructor<? extends View>> sConstructorMap = new SimpleArrayMap();
	private View createViewByPrefix(Context context, String name, String prefix) throws ClassNotFoundException, InflateException {
        Constructor constructor = (Constructor)sConstructorMap.get(name);

        try {
            if (constructor == null) {
                Class<? extends View> clazz = Class.forName(prefix != null ? prefix + name : name, false, context.getClassLoader()).asSubclass(View.class);
                constructor = clazz.getConstructor(sConstructorSignature);
                sConstructorMap.put(name, constructor);
            }

            constructor.setAccessible(true);
            return (View)constructor.newInstance(this.mConstructorArgs);
        } catch (Exception var6) {
            return null;
        }
    }

有没有觉得比较熟悉 没错 contentValuesByObj的缓存putMethod的思路就来自这里

3.补全删除 修改 查询的部分

查询:没有使用反射

不使用反射有2点弊端
1.数据库查询的地方必须知道想要查询的数据的定义(如Person.java)
2.如果数据的定义(如Person.java)发生修改 比如增加一个字段 删除一个字段 那么我们数据库的查询语句也需要修改
代码:在数据库查询的地方Copy一份Person.java

//该类是为了测试不使用反射而写的 写在这里是不合理的,使用反射的时候可以删除
@Deprecated
public class Person {

    private String name;
    private String address;
    private int age;

    public Person(String name, String address, int age) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                ", age=" + age +
                '}';
    }
}

IDaoSupport增加方法

    // 查询所有
    public List<T> queryAll();

DaoSupport实现新增方法

    // 查询所有
    @Override
    public List<T> queryAll() {
        Cursor cursor = mSqLiteDatabase.query(DaoUtil.getTableName(mClazz),null,null,null,null,null,null);
        return cursorToList(cursor);
    }

    private List<T> cursorToList(Cursor cursor) {
        List<T> list = new ArrayList<>();
        while (cursor.moveToNext()) {
            Person person = new Person(cursor.getString(cursor
                    .getColumnIndex("name")),
                    cursor.getString(cursor
                    .getColumnIndex("address")),
                    cursor.getInt(cursor
                            .getColumnIndex("age"))
            );
            list.add((T) person);//这里也是有问题的 不够通用
        }
        Log.e(TAG, "cursorToList: "+list);
        return list;
    }

在activity增加测试方法

    private void queryAll(View view) {
        new Thread(() -> {
            long startTime = System.currentTimeMillis();
            IDaoSupport<Person> daoSupport = DaoSupportFactory.getFactoryInstance(MainActivity.this).getDao(Person.class);
            List<Person> people = daoSupport.queryAll();
            Log.e(TAG, "queryAll: "+people.size());
            long endTime = System.currentTimeMillis();
            Log.e(TAG, " queryAll cost time -> " + (endTime - startTime));
        }).start();
    }

实际上 不使用反射读取数据有很多弊端,之所以这里还是列出写法 是为了对比使用反射的情况,否则反射的代码很难看懂

查询:使用反射

IDaoSupport增加方法

    // 查询所有 使用反射
    List<T> queryAllByReflect();

DaoSupport实现新增方法

    @Override
    public List<T> queryAllByReflect() {
        Cursor cursor = mSqLiteDatabase.query(DaoUtil.getTableName(mClazz), null, null, null, null, null, null);
        return cursorToListByReflect(cursor);
    }

    // 多次调用反射 查询所有列表
    private List<T> cursorToListByReflect(Cursor cursor) {
        List<T> list = new ArrayList<>();
        while (cursor.moveToNext()) {
            // 通过反射创建T对象 第一次反射 以Person为例 相当于调用Person p = new Person()
            T instance = null;// 要调用此方法 必须有无参构造方法
            try {
                instance = mClazz.newInstance();
                // 反射获取T对象所有的属性 第二次反射
                Field[] fields = mClazz.getDeclaredFields();
                // 遍历field 从数据库取出数据填充到instance
                for (Field field : fields) {
                    Object value = null;
                    field.setAccessible(true);
                    // 以Person为例 它有个属性String name, fieldName 则是 name
                    String fieldName = field.getName();
                    // 查询当前属性在数据库中所在的列 下面则相当于调用cursor.getColumnIndex("name")
                    int index = cursor.getColumnIndex(fieldName);
                    if (index != -1) {
                        // 根据对象T的属性类型推算cursor的方法 如cursor.getString cursor.getInt
                        Method cursorGetColumnMethod = convertType2CursorMethod(field.getType());// 在该方法进行第三次反射
                        // 通过反射获取 value 第四次反射 相当于cursor.getString(cursor.getColumnIndex("name"))//cursor.getColumnIndex("name")在上面已经调用
                        value = cursorGetColumnMethod.invoke(cursor, index);
                        if (value == null) {
                            continue;
                        }
                        // 处理一些特殊的部分
                        if (field.getType() == boolean.class || field.getType() == Boolean.class) {
                            // sqlite不支持bool类型 使用0代表false 1代表true
                            if ("0".equals(String.valueOf(value))) {
                                value = false;
                            } else if ("1".equals(String.valueOf(value))) {
                                value = true;
                            }
                        } else if (field.getType() == char.class || field.getType() == Character.class) {
                            // sqlite不支持char类型 取第0位即可
                            value = ((String) value).charAt(0);
                        } else if (field.getType() == Date.class) {
                            // sqlite不支持Date类型 存储的是时间戳
                            long date = (Long) value;
                            if (date <= 0) {
                                value = null;
                            } else {
                                value = new Date(date);
                            }
                        }
                    } else {
                        Log.e(TAG, "cursorToList: 该属性没有存储在数据库中");
                        continue;
                    }
                    // 反射注入属性的值(以person为例,类似调用person.setName(value))
                    // 第五次反射
                    field.set(instance, value);
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
            list.add(instance);
        }
        cursor.close();
        Log.e(TAG, "cursorToList: " + list.size());
        return list;
    }

    // 获取cursor的方法
    private Method convertType2CursorMethod(Class<?> type) throws NoSuchMethodException {
        // 根据数据类型 得到不同cursor方法
        String methodName = getColumnMethodName(type);
        // 第三次反射 根据方法名和参数类型调用
        Method method = Cursor.class.getMethod(methodName, int.class);
        return method;
    }

    private String getColumnMethodName(Class<?> fieldType) {
        String typeName;
        if (fieldType.isPrimitive()) { // 如果是基本数据类
            // 将int boolean float等转换为对象的形式 即首字母大写
            /*
             * @see     java.lang.Boolean#TYPE
             * @see     java.lang.Character#TYPE
             * @see     java.lang.Byte#TYPE
             * @see     java.lang.Short#TYPE
             * @see     java.lang.Integer#TYPE
             * @see     java.lang.Long#TYPE
             * @see     java.lang.Float#TYPE
             * @see     java.lang.Double#TYPE
             * @see     java.lang.Void#TYPE
             */
            typeName = DaoUtil.capitalize(fieldType.getName());
        } else {
            typeName = fieldType.getSimpleName();
        }
        // 上面获取对象T的Java的get方法,如Integer String Boolean 下面需要转成SQLite里面的数据类型
        // 如getBoolen转换为数据库的getInt getChar转换为数据库的getString
        String methodName = "get" + typeName;
        if ("getBoolean".equals(methodName)) {
            methodName = "getInt";
        } else if ("getChar".equals(methodName) || "getCharacter".equals(methodName)) {
            methodName = "getString";
        } else if ("getDate".equals(methodName)) {
            methodName = "getLong";
        } else if ("getInteger".equals(methodName)) {
            methodName = "getInt";
        }
        return methodName;
    }

还有个工具类方法

    // 使得参数string开头字母大写
    public static String capitalize(String string) {
        if (!TextUtils.isEmpty(string)) {
            // 前面把首字符大写 后面将剩余部分拼接上
            return string.substring(0, 1).toUpperCase(Locale.US) + string.substring(1);
        }
        return string == null ? null : "";
    }

这里面多次用到反射,其实逻辑不是很复杂 但是要又耐心看,可以对比没有反射的方法,更容易理解
注意要给Person类增加无参构造方法 否则反射调用不到无参构造方法会报错

   public Person(){

    }

最后就是测试了

    private void queryAllReflect(View view) {
        new Thread(() -> {
            long startTime = System.currentTimeMillis();
            IDaoSupport<Person> daoSupport = DaoSupportFactory.getFactoryInstance(MainActivity.this).getDao(Person.class);
            List<Person> people = daoSupport.queryAllByReflect();
            Log.e(TAG, "queryAllReflect: " + people);
            long endTime = System.currentTimeMillis();
            Log.e(TAG, " queryAllReflect cost time -> " + (endTime - startTime));
        }).start();
    }

后面还有按条件查询 更新 条件删除没有做,还有将网络与数据库结合起来的缓存机制,最后一篇数据库部分会说这些东西

标签:反射,name,数据库,Darren,cursor,value,null,红橙,String
来源: https://blog.csdn.net/u011109881/article/details/115216756