APS.NET MVC + EF (02)---深入理解ADO.NET Entity Framework
2.7 深入理解Entity Framework
性能问题几乎是一切ORM框架的通病,对于EF来说,引起性能低的原因主要在以下几个方面。
- 复杂的对象管理机制
为了在.NET中更好地管理模型对象,EF提供了一套内部机制管理和跟踪对象的状态,保持对象的一致性,带来方便的同时,降低了性能。 - 高度封装的执行机制
在EF应用中,编写的任何一个查询表达式都需要经过分析,解析成SQL语句,然后调用底层的ADO.NET Providers去执行。直接执行SQL语句相比,性能上有一定的降低。
- 低效的SQL语句
EF采用映射机制将对象操作转换为SQL语句,SQL语句一般是基于标准模板生成的,不会进行特殊的优化,这与直接针对业务编写的SQL语句去操作数据相比,效率一般会打折扣,特别是复杂的数据库操作。
Linq查询最终生成的SQL语句是什么样的?我们可以使用ToString()方法直接输出T-SQL代码。如示例10所示。
示例10
using (MySchoolContext db = new MySchoolContext()) { var result = db.Student.Where(n => n.StudentName.Contains("张")); Console.WriteLine(result); } |
运行结果如图2-10所示。
图2-10 Linq查询生成的SQL语句
当然,EF本身对性能有一系列的优化措施,会使用这写手段的前提是对EF的执行机制有足够的了解。
2.7.1 EF的状态管理
在程序中实现数据的增、删、改操作,EF会监控这些状态的变化,在执行SaveChange()方法时,会根据对象状态的变化执行相应的操作。如示例11所示。
示例11
using (MySchoolContext db = new MySchoolContext()) { Grade grade = new Grade() { GradeName = "Y3" }; //输出当前对象状态 Console.WriteLine(db.Entry(grade).State); db.Grade.Add(grade); Console.WriteLine(db.Entry(grade).State); db.SaveChanges(); Console.WriteLine(db.Entry(grade).State); } |
示例11中,通过Entry()方法获取模型状态,该方法是DbContext类的成员方法,定义如下:
public DbEntityEntry<TEntity> Entry<TEntity>(TEntity entity)
返回类型 DbEntityEntry<T> 封装了对象状态相关的信息。常用成员如表3-2所示。
表3-2 DbEntityEntry 的主要成员
方法或属性 |
说明 |
CurrentValues |
获取由此对象表示的跟踪实体的当前属性值 |
OriginalValues |
获取由此对象表示的跟踪实体的原始属性值 |
State |
获取或设置实体的状态 |
Reload() |
从数据库重新加载对象的值 |
其中,State属性是一个EntityState枚举类型,其取值如下:
Detached:表示对象存在,但没有被跟踪
Unchanged:表示对象尚未经过修改
Added:表示对象为新对象,并且已添加到对象上下文
Deleted:对象已从对象上下文中删除
Modified:表示对象的一个标量属性已更改
通过设置实体的State属性也可以实现对数据的操作,如:
using (MySchoolContext db = new MySchoolContext()) { Grade grade = new Grade() { GradeName = "Y3" }; db.Entry(grade).State = EntityState.Added; db.SaveChanges( ); } |
除了需要增删改的对象会受状态管理机制管理外,通过EF查询的数据也默认会进行状态管理。可以通过两种方式指定查询不进行状态管理。
方式一:使用AsNoTracking()方法,如示例12所示。
示例12
using (MySchoolContext db = new MySchoolContext()) { var result = db.Student.AsNoTracking().FirstOrDefault(); Console.WriteLine(db.Entry(result).State); } |
方式二:设置Configuration.AutoDetectChangesEnabled 属性为false,如示例13。
示例13
using (MySchoolContext db = new MySchoolContext()) { //禁用自动跟踪变化 db.Configuration.AutoDetectChangesEnabled = false; for (int i = 0; i < 5000; i++) { var stu = new Student() { StudentName = "alex", GradeId = 1, Age = 20 }; db.Student.Add(stu); } db.SaveChanges(); } |
在使用EF修改或删除数据时,必须先查询对象,然后再对其进行修改或删除。然而现实开发中很多情况都是通过主键删除一条数据。我们可以通过实体的状态特性来进行优化。
示例14
using (MySchool1Entities entities = new MySchool1Entities()) { //创建替身对象 var stu = new Student { StudentNo = "10001" }; //给实体附加到上下文中 entities.Student.Attach(stu); //删除 entities.Student.Remove(stu); entities.SaveChanges(); } |
代码中的Attach()方法可以将EntityState.Unchangee状态的对象附加到上下文中。
2.7.2 延迟加载和贪婪加载
- 延迟加载
又称为懒加载,只有每次调用子实体(外键所在的实体)的时候,才去查询数据库, 主表数据加载的时候,不去查询外键所在的从表。
实现延迟加载需要满足两个条件:
- poco类是public且不能为sealed。
- 导航属性需要标记为Virtual。
也可以关闭延迟加载,方法是:
db.Configuration.LazyLoadingEnabled = false;
关闭延迟加载后,查询主表数据时候,主表的中从表实体为null。
示例15
using (dbContext1 db = new dbContext1()) { Console.WriteLine("---------------- 01-延迟加载 ---------------"); //EF默认就是延迟加载,默认下面的语句就是true,所以下面语句注释没有任何影响 db.Configuration.LazyLoadingEnabled = true; var list = db.Student.ToList(); //此处加载的数据,没有对从表进行任何查询操作 foreach (var stu in list) { Console.WriteLine("学生编号:{0},学生姓名:{1}", stu.studentId, stu.studentName); //下面调用导航属性(一对一的关系) 每次调用时,都要去查询数据库 var stuAddress = stu.StudentAddress; Console.WriteLine("地址编号:{0},地址名称:{1}", stuAddress.studentAddressId, stu.studentName); } } |
- 贪婪加载
又名:立即加载、贪婪加载、预加载。查询主表的时候通过Include()方法一次性将数据查询了出来,在调用从表数据的时候,从缓存中读取,无须查询数据库。
实现方式:
- 先关闭延迟加载:db.Configuration.LazyLoadingEnabled = false;
- 查询主表的同时通过Include把从表数据也查询出来。
示例16
using (dbContext1 db = new dbContext1()) { Console.WriteLine("------------------- 03-立即加载 ------------------"); //1.关闭延迟加载 db.Configuration.LazyLoadingEnabled = false; //2. 获取主表数据的同时,通过Include将从表中的数据也全部加载出来 var list = db.Student.Include("StudentAddress").ToList(); foreach (var stu in list) { Console.WriteLine("学生编号:{0},学生姓名:{1}", stu.studentId, stu.studentName); //这里获取从表中的数据,均是从缓存中获取,无需查询数据库 var stuAddress = stu.StudentAddress; Console.WriteLine("地址编号:{0},地址名称:{1}", stuAddress.studentAddressId, stu.studentName); } } |
- 显示加载
关闭了延迟加载,单纯查询了主表数据,这个时候需要重新查询从表数据,就要用到显式加载了。
使用步骤:
①:单个实体用:Reference
②:集合用:Collection
③:最后需要Load一下
示例17
using (dbContext1 db = new dbContext1()) { Console.WriteLine("----------------- 04-显式加载 ------------------"); //1.关闭延迟加载 db.Configuration.LazyLoadingEnabled = false; //2.此处加载的数据,不含从表中的数据 var list = db.Student.ToList(); foreach (var stu in list) { Console.WriteLine("学生编号:{0},学生姓名:{1}", stu.studentId, stu.studentName); //3.下面的这句话,可以开启重新查询一次数据库 //3.1 单个属性的情况用Refercence db.Entry<Student>(stu).Reference(c => c.StudentAddress).Load(); //3.2 集合的情况用Collection //db.Entry<Student>(stu).Collection(c => c.StudentAddress).Load(); //下面调用导航属性(一对一的关系) 每次调用时,都要去查询数据库 var stuAddress = stu.StudentAddress; Console.WriteLine("地址编号:{0},地址名称:{1}", stuAddress.studentAddressId, stu.studentName); } } |
- 小结
什么时候使用延迟加载,什么时候又使用贪婪加载呢?
延迟加载只有在需要使用数据时加载,如果不需要使用实体的关联数据,可以使用延迟加载。使用贪婪加载适用于预先了解要使用什么数据的情况,利用这种方式一次性加载数据,可以减少数据库访问次数。
从实际情况来看,使用默认的延迟加载就可以了,2次或3次的数据库查询是可以接受的。而循环中多次读取数据库,可以考虑使用贪婪加载。
2.7.3 本地缓存
在使用EF时,有时会多次使用一个查询结果,如示例18所示。
示例18
using (MySchoolEntities db = new MySchoolEntities()) { //查询全部学生 IQueryable<Student> stus = db.Student; Console.WriteLine("全部学生姓名:"); foreach (var stu in stus) { Console.WriteLine("学生姓名:{0}",stu.StudentName); } //查询并输出学生人数 Console.WriteLine("学生人数为:{0}",db.Student.Count()); } |
示例18中会产生两次查询,但从需求来看,完全没有必要,因为第二次完全可以利用第一次查询的结果。事实上,完全可以使用EF的缓存功能,直接利用缓存的结果,DbSet<T>的Local属性正是用于提供缓存的数据。
示例18中,将"db.Student.Count()"替换为"db.Student.Local.Count()",这样就不会产生新的查询了。
另外,DbSet<T>提供了 Find()方法,用于通过主键查找实体,其查询速度比First()和FirstOrDefault()方法快的多,并且如果相应的实体已经被DbContext缓存,EF会在缓存中直接返回对应的实体,而不会执行数据库访问。
2.7.4 EF中的事务
EF中的事务主要分为三类,分别是SaveChanges、DbContextTransaction 和
TransactionScope。
- SaveChanges事务
在前面内容中,SaveChanges一次性将本地缓存中所有的状态变化一次性提交到数据库,这就是一个事务,要么统一成功,要么统一回滚。
示例19
using (DbContext db = new CodeFirstModel()) { //增加 TestInfor t1 = new TestInfor() { id = Guid.NewGuid().ToString("N"), txt1 = "txt1", txt2 = "txt2" }; db.Set<TestInfor>().Add(t1); //删除 TestInfor t2 = db.Set<TestInfor>().Where(u => u.id == "1").FirstOrDefault(); if (t2 != null) { db.Set<TestInfor>().Remove(t2); } //修改 TestInfor t3 = db.Set<TestInfor>().Where(u => u.id == "3").FirstOrDefault(); t3.txt2 = "我是李马茹23"; //SaveChanges事务提交 int n = db.SaveChanges(); Console.WriteLine("数据作用条数:" + n); } |
示例19中,如果三个操作中有任意一个出现错误,就会回滚,结果n为0。
- DbContextTransaction 事务
使用场景:EF调用SQL语句的时候使用该事务、 多个SaveChanges的情况。
示例20
using (DbContext db = new CodeFirstModel()) { DbContextTransaction trans = null; try { //开启事务 trans = db.Database.BeginTransaction(); //增加 string sql1 = @"insert into TestInfor values(@id,@txt1,@txt2)"; SqlParameter[] pars1 ={ new SqlParameter("@id",Guid.NewGuid().ToString("N")), new SqlParameter("@txt1","txt11"), new SqlParameter("@txt2","txt22") }; db.Database.ExecuteSqlCommand(sql1, pars1); //删除 string sql2 = @"delete from TestInfor where id=@id"; SqlParameter[] pars2 ={ new SqlParameter("@id","22") }; db.Database.ExecuteSqlCommand(sql2, pars2); //修改 string sql3 = @"update TestInfor set txt1=@txt1 where id=@id"; SqlParameter[] pars3 ={ new SqlParameter("@id","3"), new SqlParameter("@txt1","二狗子") }; db.Database.ExecuteSqlCommand(sql3, pars3); //提交事务 trans.Commit(); Console.WriteLine("事务成功了"); } catch (Exception ex) { Console.WriteLine(ex.Message); trans.Rollback(); //回滚 } finally { //也可以把该事务写到using块中,让其自己托管,就不需要手动释放了 trans.Dispose(); } } |
DbContextTransaction事务也适用于多个SaveChanges的情况。
示例21
using (DbContext db = new CodeFirstModel()) { //自动脱管,不需要手动释放 using (DbContextTransaction trans = db.Database.BeginTransaction()) { try { TestInfor t1 = new TestInfor() { id = Guid.NewGuid().ToString("N"), txt1 = "111111111", txt2 = "222222222222" }; db.Entry(t1).State = EntityState.Added; db.SaveChanges(); TestInfor t2 = new TestInfor() { id = Guid.NewGuid().ToString("N") + "123", txt1 = "111111111", txt2 = "222222222222" }; db.Entry(t2).State = EntityState.Added; db.SaveChanges(); trans.Commit(); } catch (Exception) { trans.Rollback(); } } } |
- TransactionScope事务
该种事务适用于多数据库连接的情况,在此不做介绍,请自行查阅相关资料。
2.7.5 从实体框架回归SQL
EF虽然本身有很多优化机制,但和直接使用ADO.NET相比,还是有一定的性能差距,因此EF在DbContext类的Database属性里提供了ExecuteSqlCommand()和SqlQuery()两个方法,用来直接访问数据库。
- ExecuteSqlCommand()
方法的定义如下:
public int ExecuteSqlCommand(string sql, params object[] parameters)
用来执行增、删、改操作,返回结果为受影响行数。
- SqlQuery()
方法的定义如下:
public DbRawSqlQuery<T> SqlQuery<T>(string sql, params object[] parameters);
用来执行查询操作,返回结果是一个集合。
示例22
using (MySchool1Entities db = new MySchool1Entities()) { //执行update语句 string sql = "update grade set gradeName=@gradeName where gradeId=@gradeId"; SqlParameter[] ps = { new SqlParameter("@gradeName","第二学年"), new SqlParameter("@gradeId",3) }; int result=db.Database.ExecuteSqlCommand(sql, ps); if (result>0) { Console.WriteLine("数据更新完成!"); } //执行查询语句 sql = "select * from from student where studentNo=@stuNo"; ps = new SqlParameter[] { new SqlParameter("@stuNo", "S1001234") }; var stu = db.Database.SqlQuery<Student>(sql, ps); Console.WriteLine(stu.ToList()[0]); } |
2.8 封装EF的DAL层
public class BaseDAL<T> where T:class
{
private DbContext db
{
get
{
DbContext dbContext = CallContext.GetData("dbContext") as DbContext;
if (dbContext == null)
{
dbContext = new MySchoolContext();
CallContext.SetData("dbContext", dbContext);
}
return dbContext;
}
} /// <summary>
/// 执行增加,删除,修改操作(或调用存储过程)
/// </summary>
/// <param name="sql"></param>
/// <param name="pars"></param>
/// <returns></returns>
public int ExecuteSql(string sql, params SqlParameter[] pars)
{
return db.Database.ExecuteSqlCommand(sql, pars);
} /// <summary>
/// 执行查询操作
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="sql"></param>
/// <param name="pars"></param>
/// <returns></returns>
public List<T> ExecuteQuery(string sql, params SqlParameter[] pars)
{
return db.Database.SqlQuery<T>(sql, pars).ToList();
} /// <summary>
/// 添加
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
public int Add(T model)
{
db.Set<T>().Add(model);
return db.SaveChanges();
} /// <summary>
/// 删除(适用于先查询后删除的单个实体)
/// </summary>
/// <param name="model">需要删除的实体</param>
/// <returns></returns>
public int Del(T model)
{
db.Set<T>().Attach(model);
db.Set<T>().Remove(model);
return db.SaveChanges();
} /// <summary>
/// 根据条件删除(支持批量删除)
/// </summary>
/// <param name="delWhere">传入Lambda表达式(生成表达式目录树)</param>
/// <returns></returns>
public int DelBy(Expression<Func<T, bool>> delWhere)
{
var listDels = db.Set<T>().Where(delWhere);
foreach(var model in listDels)
{
db.Set<T>().Attach(model);
db.Set<T>().Remove(model);
}
return db.SaveChanges();
} /// <summary>
/// 修改
/// </summary>
/// <param name="model">修改后的实体</param>
/// <returns></returns>
public int Modify(T model)
{
db.Entry(model).State = EntityState.Modified;
return db.SaveChanges();
} /// <summary>
/// 批量修改
/// </summary>
/// <param name="model">要修改实体中 修改后的属性 </param>
/// <param name="whereLambda">查询实体的条件</param>
/// <param name="proNames">lambda的形式表示要修改的实体属性名</param>
/// <returns></returns>
public int ModifyBy(T model, Expression<Func<T, bool>> whereLambda, params string[] proNames)
{
List<T> listModifes = db.Set<T>().Where(whereLambda).ToList();
Type t = typeof(T);
List<PropertyInfo> proInfos = t.GetProperties(BindingFlags.Instance | BindingFlags.Public).ToList();
Dictionary<string, PropertyInfo> dicPros = new Dictionary<string, PropertyInfo>();
proInfos.ForEach(p =>
{
if (proNames.Contains(p.Name))
{
dicPros.Add(p.Name, p);
}
});
foreach (string proName in proNames)
{
if (dicPros.ContainsKey(proName))
{
PropertyInfo proInfo = dicPros[proName];
object newValue = proInfo.GetValue(model, null);
foreach (T m in listModifes)
{
proInfo.SetValue(m, newValue, null);
}
}
}
return db.SaveChanges();
} /// <summary>
/// 根据条件查询
/// </summary>
/// <param name="whereLambda">查询条件(lambda表达式的形式生成表达式目录树)</param>
/// <returns></returns>
public IQueryable<T> GetListBy(Expression<Func<T, bool>> whereLambda)
{
return db.Set<T>().Where(whereLambda);
}
/// <summary>
/// 根据条件排序和查询
/// </summary>
/// <typeparam name="Tkey">排序字段类型</typeparam>
/// <param name="whereLambda">查询条件</param>
/// <param name="orderLambda">排序条件</param>
/// <param name="isAsc">升序or降序</param>
/// <returns></returns>
public IQueryable<T> GetListBy<Tkey>(Expression<Func<T, bool>> whereLambda, Expression<Func<T, Tkey>> orderLambda, bool isAsc = true)
{
if (isAsc)
{
return db.Set<T>().Where(whereLambda).OrderBy(orderLambda);
}
else
{
return db.Set<T>().Where(whereLambda).OrderByDescending(orderLambda);
}
}
/// <summary>
/// 分页查询
/// </summary>
/// <typeparam name="Tkey">排序字段类型</typeparam>
/// <param name="pageIndex">页码</param>
/// <param name="pageSize">页容量</param>
/// <param name="whereLambda">查询条件</param>
/// <param name="orderLambda">排序条件</param>
/// <param name="isAsc">升序or降序</param>
/// <returns></returns>
public IQueryable<T> GetPageList<Tkey>(int pageIndex, int pageSize, Expression<Func<T, bool>> whereLambda, Expression<Func<T, Tkey>> orderLambda, bool isAsc = true)
{ IQueryable<T> list = null;
if (isAsc)
{
list = db.Set<T>().Where(whereLambda).OrderBy(orderLambda)
.Skip((pageIndex - ) * pageSize).Take(pageSize);
}
else
{
list = db.Set<T>().Where(whereLambda).OrderByDescending(orderLambda)
.Skip((pageIndex - ) * pageSize).Take(pageSize);
}
return list;
}
/// <summary>
/// 分页查询输出总行数
/// </summary>
/// <typeparam name="Tkey">排序字段类型</typeparam>
/// <param name="pageIndex">页码</param>
/// <param name="pageSize">页容量</param>
/// <param name="whereLambda">查询条件</param>
/// <param name="orderLambda">排序条件</param>
/// <param name="isAsc">升序or降序</param>
/// <returns></returns>
public IQueryable<T> GetPageList<Tkey>(int pageIndex, int pageSize, out int rowCount, Expression<Func<T, bool>> whereLambda, Expression<Func<T, Tkey>> orderLambda, bool isAsc = true)
{
IQueryable<T> list = null;
rowCount = db.Set<T>().Where(whereLambda).Count();
if (isAsc)
{
list = db.Set<T>().Where(whereLambda).OrderBy(orderLambda)
.Skip((pageIndex - ) * pageSize).Take(pageSize);
}
else
{
list = db.Set<T>().Where(whereLambda).OrderByDescending(orderLambda)
.Skip((pageIndex - ) * pageSize).Take(pageSize);
}
return list;
} }
<<BaseDAL.cs>>
APS.NET MVC + EF (02)---深入理解ADO.NET Entity Framework的更多相关文章
- APS.NET MVC + EF (02)---ADO.NET Entity FrameWork
2.1 Entity Framework简介 Ado.net Entity Framework 是Microsoft推出的ORM框架. 2.1.1 什么是ORM 对象关系映射(Object Relat ...
- ADO.NET Entity Framework CodeFirst 如何输出日志(EF 5.0)
ADO.NET Entity Framework CodeFirst 如何输出日志(EF4.3) 用的EFProviderWrappers ,这个组件好久没有更新了,对于SQL执行日志的解决方案的需求 ...
- ADO.NET Entity Framework(EF)
ylbtech-Miscellaneos: ADO.NET Entity Framework(EF) A,返回顶部 1, ADO.NET Entity Framework 是微软以 ADO.NET 为 ...
- Entity FrameWork(实体框架)是以ADO.NET Entity FrameWork ,简称为EF
Entity FrameWork(实体框架)是以ADO.NET Entity FrameWork ,简称为EF Entity FrameWork的特点 1.支持多种数据库(MSSQL.Oracle.M ...
- 如何得到EF(ADO.NET Entity Framework)查询生成的SQL? ToTraceString Database.Log
ADO.NET Entity Framework ToTraceString //输出单条查询 DbContext.Database.Log //这里有详细的日志
- Microsoft SQL Server Compact 4.0&&ADO.NET Entity Framework 4.1&&MVC3
最近重新查看微软MvcMusicStore-v3.0的源代码,发现忽略了很多重要的东西,特别是数据访问那一部分. 首先Microsoft SQL Server Compact 4.0 详细的介绍和下载 ...
- ADO.NET Entity Framework 在哪些场景下使用?
在知乎回答了下,顺手转回来. Enity Framework已经是.NET下最主要的ORM了.而ORM从一个Mapping的概念开始,到现在已经得到了一定的升华,特别是EF等对ORM框架面向对象能力的 ...
- ADO.NET Entity Framework学习笔记(3)ObjectContext
ADO.NET Entity Framework学习笔记(3)ObjectContext对象[转] 说明 ObjectContext提供了管理数据的功能 Context操作数据 AddObject ...
- 让ADO.NET Entity Framework 支持ACCESS数据库
如写的不好请见谅,本人水平有限. 个人简历及水平:. http://www.cnblogs.com/hackdragon/p/3662599.html 接到一个程序和网页交互的项目,用ADO.NET ...
随机推荐
- Nginx 核心配置-单节点实现多域名访问
Nginx 核心配置-单节点实现多域名访问 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.试验环境说明 1>.虚拟机环境说明 [root@node101.yinzheng ...
- Win10开启快速启动后关机变重启
同样可以用来解决,“msconfig引导为空”,“默认操作系统为空”,“win10改为uefi启动后关机变重启”,“legacy转uefi后无法关机” 问题起因 换完主板和cpu后,新的主板在开机时总 ...
- springMVC学习2
参数绑定 默认支持的参数类型 @Override public Item queryItemById(int id) { Item item = this.itemMapper.selectByPri ...
- LeetCode 885. Spiral Matrix III
原题链接在这里:https://leetcode.com/problems/spiral-matrix-iii/ 题目: On a 2 dimensional grid with R rows and ...
- 原生php分页的封装,只封装函数,可适用所有的表
<?php/** * 封装分页函数 * $table [字符串] 表名 * @$size [数字][每页显示条数] */function fenye($table, $size){ $link ...
- Chrome插件中 popup,background,contentscript消息传递机制
https://blog.csdn.net/summerxiachen/article/details/78698878 popup不能接收到contentscript 发送的消息, 如果这时cont ...
- selenium--加载浏览器配置
前戏 在我们之前写的自动化脚本中,不知道大家有没有发现,每次打开的都是一个新的浏览器(相当于新安装的).但是有时候,我们想打开的是我们配置好的浏览器.我在之前的公司做web自动化的时候,由于我们的网站 ...
- 使用Java8特性对list进行按属性去重
编写工具类 public class DistinctUtil { public static <T> Predicate<T> distinctByKey(Function& ...
- orcle not like不建议使用(not like所踩过的坑!)
1.情景展示 现在有一张表,需要将表中某字段的值不是以指定字符开头的列进行删除,如何实现? 2.问题分析 错误方案一:同事想到的是:这种方式 咱们来看一下,这个表总共有多少条数据 本来表数据总共才 ...
- 每日一问:不一样的角度吐槽下 DataBinding
我们项目采用的是 kotlin && DataBinding 处理的,可能你会疑问,既然用的是 kotlin,为啥没有用 kotlinx?新的页面当然是用的 kotlinx 啦,但我们 ...