.NET ORM 仓储层必备的功能介绍之 FreeSql Repository 实现篇
写在开头
2018年11月的某一天,头脑发热开启了 FreeSql 开源项目之旅,时间一晃已经四年多,当初从舒服区走向一个巨大的坑,回头一看后背一凉。四年时间从无到有,经历了数不清的日夜奋战(有人问我花了多长时间投入,答案:全职x2 + 前两年无休息,以及后面两年的持续投入)。今天 FreeSql 已经很强大,感谢第一期、第二期、第N期持续提出建议的网友。
FreeSql 现如今已经是一个稳定的版本,主要体现:
- API 已经确定,不会轻易推翻重作调整,坚持十年不变的原则,让使用者真真正正的不再关心 ORM 使用问题;
- 单元测试覆盖面广,6336+ 个单元测试,小版本更新升级无须考虑修东墙、补西墙的问题;
- 经历四年时间的生产考验,nuget下载量已超过900K+,平均每日750+;
感叹:有些人说 .Net 陷入 orm 怪圈,动手的没几个,指点江山的一堆,.Net orm 真的如他们所讲的简单吗?
项目介绍
FreeSql 是 .Net ORM,能支持 .NetFramework4.0+、.NetCore、Xamarin、MAUI、Blazor、以及还有说不出来的运行平台,因为代码绿色无依赖,支持新平台非常简单。目前单元测试数量:6336+,Nuget下载数量:900K+。QQ群:4336577(已满)、8578575(在线)、52508226(在线)
温馨提醒:以下内容无商吹成份,FreeSql 不打诳语
为什么要重复造轮子?
FreeSql 主要优势在于易用性上,基本是开箱即用,在不同数据库之间切换兼容性比较好。作者花了大量的时间精力在这个项目,肯请您花半小时了解下项目,谢谢。FreeSql 整体的功能特性如下:
- 支持 CodeFirst 对比结构变化迁移;
- 支持 DbFirst 从数据库导入实体类;
- 支持 丰富的表达式函数,自定义解析;
- 支持 批量添加、批量更新、BulkCopy;
- 支持 导航属性,贪婪加载、延时加载、级联保存;
- 支持 读写分离、分表分库,租户设计;
- 支持 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/Firebird/达梦/神通/人大金仓/翰高/MsAccess Ado.net 实现包,以及 Odbc 的专门实现包;
5500+个单元测试作为基调,支持10多数数据库,我们提供了通用Odbc理论上支持所有数据库,目前已知有群友使用 FreeSql 操作华为高斯、mycat、tidb 等数据库。安装时只需要选择对应的数据库实现包:
dotnet add packages FreeSql.Provider.MySql
FreeSql.Repository 是 FreeSql 项目的延申扩展类库,支持 .NETFramework4.0+、.NETCore2.0+、.NET5+、Xamarin 平台。
FreeSql.Repository 除了 CRUD 还有很多实用性功能,不防耐下心花10分钟看完。
01 安装
环境1:.NET Core 或 .NET 5.0+
dotnet add package FreeSql.Repository
环境2、.NET Framework
Install-Package FreeSql.DbContext
static IFreeSql fsql = new FreeSql.FreeSqlBuilder().UseConnectionString(FreeSql.DataType.Sqlite, connectionString).UseAutoSyncStructure(true) //自动迁移实体的结构到数据库.Build(); //请务必定义成 Singleton 单例模式
02 使用方法
方法1、IFreeSql 的扩展方法;
var curd = fsql.GetRepository<Topic>();
注意:Repository 对象多线程不安全,因此不应在多个线程上同时对其执行工作。
- fsql.GetRepository 方法返回新仓储实例
- 不支持从不同的线程同时使用同一仓储实例
以下为了方便测试代码演示,我们都使用方法1,fsql.GetRepository 创建新仓储实例
方法2、继承实现;
public class TopicRepository: BaseRepository<Topic, int> {public TopicRepository(IFreeSql fsql) : base(fsql, null, null) {}//在这里增加 CURD 以外的方法
}
方法3、依赖注入;
public void ConfigureServices(IServiceCollection services)
{services.AddSingleton<IFreeSql>(Fsql);services.AddFreeRepository(null, this.GetType().Assembly);
}//在控制器使用
public TopicController(IBaseRepository<Topic> repo) {}
03 添加数据
repo.Insert 插入数据,适配了各数据库优化执行 ExecuteAffrows/ExecuteIdentity/ExecuteInserted
1、如果表有自增列,插入数据后应该要返回 id。
var repo = fsql.GetRepository<Topic>();
repo.Insert(topic);
内部会将插入后的自增值填充给 topic.Id
2、批量插入
var repo = fsql.GetRepository<Topic>();
var topics = new [] { new Topic { ... }, new Topic { ... } };
repo.Insert(topics);
3、插入数据库时间
使用 [Column(ServerTime = DateTimeKind.Utc)] 特性,插入数据时,使用适配好的每种数据库内容,如 getutcdate()
4、插入特殊类型
使用 [Column(RereadSql = “{0}.STAsText()”, RewriteSql = “geography::STGeomFromText({0},4236)”)] 特性,插入和读取时特别处理
5、插入时忽略
使用 [Column(CanInsert = false)] 特性
04 更新数据
1、只更新变化的属性
var repo = fsql.GetRepository<Topic>();
var item = repo.Where(a => a.Id == 1).First(); //此时快照 itemitem.Name = "newtitle";
repo.Update(item); //对比快照时的变化
//UPDATE `Topic` SET `Title` = 'newtitle'
//WHERE (`Id` = 1)
2、手工管理状态
var repo = fsql.GetRepository<Topic>();
var item = new Topic { Id = 1 };
repo.Attach(item); //此时快照 itemitem.Title = "newtitle";
repo.Update(item); //对比快照时的变化
//UPDATE `Topic` SET `Title` = 'newtitle'
//WHERE (`Id` = 1)
3、直接使用 repo.UpdateDiy,它是 IFreeSql 提供的原生 IUpdate 对象,功能更丰富
05 级联保存数据
实践发现,N对1 不适合做级联保存。保存 Topic 的时候把 Type 信息也保存?我个人认为自下向上保存的功能太不可控了,FreeSql 目前不支持自下向上保存。因此下面我们只讲 OneToOne/OneToMany/ManyToMany 级联保存。至于 ManyToOne 级联保存使用手工处理,更加安全可控。
功能1:SaveMany 手工保存
完整保存,对比表已存在的数据,计算出添加、修改、删除执行。
递归保存导航属性不安全,不可控,并非技术问题,而是出于安全考虑,提供了手工完整保存的方式。
var repo = fsql.GetRepository<Type>();
var type = new Type
{name = "c#",Topics = new List<Topic>(new[]{new Topic { ... }})
};
repo.Insert(type);
repo.SaveMany(type, "Topics"); //手工完整保存 Topics
- SaveMany 仅支持 OneToMany、ManyToMany 导航属性
- 只保存 Topics,不向下递归追朔
- 当 Topics 为 Empty 时,删除 type 存在的 Topics 所有表数据,确认?
- ManyToMany 机制为,完整对比保存中间表,外部表只追加不更新
如:
- 本表 Topic
- 外部表 Tag
- 中间表 TopicTag
功能2:EnableCascadeSave 仓储级联保存
DbContext/Repository EnableCascadeSave 可实现保存对象的时候,递归追朔其 OneToOne/OneToMany/ManyToMany 导航属性也一并保存,本文档说明机制防止误用。
1、OneToOne 级联保存
v3.2.606+ 支持,并且支持级联删除功能(文档请向下浏览)
2、OneToMany 追加或更新子表,不删除子表已存在的数据
var repo = fsql.GetRepository<Type>();
repo.DbContextOptions.EnableCascadeSave = true; //需要手工开启
repo.Insert(type);
- 不删除 Topics 子表已存在的数据,确认?
- 当 Topics 属性为 Empty 时,不做任何操作,确认?
- 保存 Topics 的时候,还会保存 Topics[0-…] 的下级集合属性,向下18层,确认?
向下18层的意思,比如【类型】表,下面有集合属性【文章】,【文章】下面有集合属性【评论】。
保存【类型】表对象的时候,他会向下检索出集合属性【文章】,然后如果【文章】被保存的时候,再继续向下检索出集合属性【评论】。一起做 InsertOrUpdate 操作。
3、ManyToMany 完整对比保存中间表,追加外部表
完整对比保存中间表,对比【多对多】中间表已存在的数据,计算出添加、修改、删除执行。
追加外部表,只追加不更新。
- 本表 Topic
- 外部表 Tag
- 中间表 TopicTag
06 删除数据
var repo = fsql.GetRepository<Topic>();
repo.Delete(new Topic { Id = 1 }); //有重载方法 repo.Delete(Topic[])var repo2 = fsql.GetRepository<Topic, int>(); //int 是主键类型,相比 repo 对象多了 Delete(int) 方法
repo2.Delete(1);
07 级联删除数据
第一种:基于【对象】级联删除
比如使用过 Include/IncludeMany 查询的对象,可以使用此方法级联删除它们。
var repo = fsql.GetRepository<Group>();
repo.DbContextOptions.EnableCascadeSave = true; //关键设置
repo.Insert(new UserGroup
{GroupName = "group01",Users = new List<User>{new User { Username = "admin01", Password = "pwd01", UserExt = new UserExt { Remark = "用户备注01" } },new User { Username = "admin02", Password = "pwd02", UserExt = new UserExt { Remark = "用户备注02" } },new User { Username = "admin03", Password = "pwd03", UserExt = new UserExt { Remark = "用户备注03" } },}
}); //级联添加测试数据
//INSERT INTO "usergroup"("groupname") VALUES('group01') RETURNING "id"
//INSERT INTO "user"("username", "password", "groupid") VALUES('admin01', 'pwd01', 1), ('admin02', 'pwd02', 1), ('admin03', 'pwd03', 1) RETURNING "id" as "Id", "username" as "Username", "password" as "Password", "groupid" as "GroupId"
//INSERT INTO "userext"("userid", "remark") VALUES(3, '用户备注01'), (4, '用户备注02'), (5, '用户备注03')var groups = repo.Select.IncludeMany(a => a.Users, then => then.Include(b => b.UserExt)).ToList();
repo.Delete(groups); //级联删除,递归向下遍历 group OneToOne/OneToMany/ManyToMany 导航属性
//DELETE FROM "userext" WHERE ("userid" IN (3,4,5))
//DELETE FROM "user" WHERE ("id" IN (3,4,5))
//DELETE FROM "usergroup" WHERE ("id" = 1)
第二种:基于【数据库】级联删除,不依赖数据库外键
根据设置的导航属性,递归删除 OneToOne/OneToMany/ManyToMany 对应数据,并返回已删除的数据。此功能不依赖数据库外键
var repo = fsql.GetRepository<Group>();
var ret = repo.DeleteCascadeByDatabase(a => a.Id == 1);
//SELECT a."id", a."username", a."password", a."groupid" FROM "user" a WHERE (a."groupid" = 1)
//SELECT a."userid", a."remark" FROM "userext" a WHERE (a."userid" IN (3,4,5))
//DELETE FROM "userext" WHERE ("userid" IN (3,4,5))
//DELETE FROM "user" WHERE ("id" IN (3,4,5))
//DELETE FROM "usergroup" WHERE ("id" = 1)//ret Count = 7 System.Collections.Generic.List<object>
// [0] {UserExt} object {UserExt}
// [1] {UserExt} object {UserExt}
// [2] {UserExt} object {UserExt}
// [3] {User} object {User}
// [4] {User} object {User}
// [5] {User} object {User}
// [6] {UserGroup} object {UserGroup}public class Group
{[Column(IsIdentity = true)]public int Id { get; set; }public string GroupName { get; set; }[Navigate(nameof(User.GroupId))]public List<User> Users { get; set; }
}
public class User
{[Column(IsIdentity = true)]public int Id { get; set; }public string Username { get; set; }public string Password { get; set; }public int GroupId { get; set; }[Navigate(nameof(Id))]public UserExt UserExt { get; set; }
}
public class UserExt
{[Column(IsPrimary = true)]public int UserId { get; set; }public string Remark { get; set; }[Navigate(nameof(UserId))]public User User { get; set; }
}
08 添加或修改数据
var repo = fsql.GetRepository<Topic>();
repo.InsertOrUpdate(item);
如果内部的状态管理存在数据,则更新。
如果内部的状态管理不存在数据,则查询数据库,判断是否存在。
存在则更新,不存在则插入
缺点:不支持批量操作
提醒:IFreeSql 也定义了 InsertOrUpdate 方法,两者实现机制不同,它利用了数据库特性:
Database | Features | Database | Features | |
---|---|---|---|---|
MySql | on duplicate key update | 达梦 | merge into | |
PostgreSQL | on conflict do update | 人大金仓 | on conflict do update | |
SqlServer | merge into | 神通 | merge into | |
Oracle | merge into | 南大通用 | merge into | |
Sqlite | replace into | MsAccess | 不支持 | |
Firebird | merge into |
fsql.InsertOrUpdate<T>().SetSource(items) //需要操作的数据//.IfExistsDoNothing() //如果数据存在,啥事也不干(相当于只有不存在数据时才插入).ExecuteAffrows();
09 批量编辑数据
var repo = fsql.GetRepository<BeginEdit01>();
var cts = new[] {new BeginEdit01 { Name = "分类1" },new BeginEdit01 { Name = "分类1_1" },new BeginEdit01 { Name = "分类1_2" },new BeginEdit01 { Name = "分类1_3" },new BeginEdit01 { Name = "分类2" },new BeginEdit01 { Name = "分类2_1" },new BeginEdit01 { Name = "分类2_2" }
}.ToList();
repo.Insert(cts);repo.BeginEdit(cts); //开始对 cts 进行编辑cts.Add(new BeginEdit01 { Name = "分类2_3" });
cts[0].Name = "123123";
cts.RemoveAt(1);var affrows = repo.EndEdit(); //完成编辑
Assert.Equal(3, affrows);
class BeginEdit01
{public Guid Id { get; set; }public string Name { get; set; }
}
上面的代码 EndEdit 方法执行的时候产生 3 条 SQL 如下:
INSERT INTO "BeginEdit01"("Id", "Name") VALUES('5f26bf07-6ac3-cbe8-00da-7dd74818c3a6', '分类2_3')UPDATE "BeginEdit01" SET "Name" = '123123'
WHERE ("Id" = '5f26bf00-6ac3-cbe8-00da-7dd01be76e26')DELETE FROM "BeginEdit01" WHERE ("Id" = '5f26bf00-6ac3-cbe8-00da-7dd11bcf54dc')
场景:winform 加载表数据后,一顿添加、修改、删除操作之后,点击【保存】
提醒:该操作只对变量 cts 有效,不是针对全表对比更新。
10 弱类型 CRUD
var repo = fsql.GetRepository<object>();
repo.AsType(typeof(Topic));var item = (object)new Topic { Title = "object title" };
repo.Insert(item);
11 无参数化命令
支持参数化、无参数化命令执行,有一些特定的数据库,使用无参数化命令执行效率更高哦,并且调试起来更直观。
var repo = fsql.GetRepository<object>();
repo.DbContextOptions.NoneParameter = true;
12 工作单元(事务)
UnitOfWork 可将多个仓储放在一个单元管理执行,最终通用 Commit 执行所有操作,内部采用了数据库事务。
方法1:随时创建使用
using (var uow = fsql.CreateUnitOfWork())
{var typeRepo = fsql.GetRepository<Type>();var topicRepo = fsql.GetRepository<Topic>();typeRepo.UnitOfWork = uow;topicRepo.UnitOfWork = uow;typeRepo.Insert(new Type());topicRepo.Insert(new Topic());uow.Orm.Insert(new Topic()).ExecuteAffrows();//uow.Orm 和 fsql 都是 IFreeSql//uow.Orm CRUD 与 uow 是一个事务//fsql CRUD 与 uow 不在一个事务uow.Commit();
}
方法2:使用 AOP + UnitOfWorkManager 实现多种事务传播
本段内容引导,如何在 asp.net core 项目中使用特性(注解) 的方式管理事务。
UnitOfWorkManager 支持六种传播方式(propagation),意味着跨方法的事务非常方便,并且支持同步异步:
- Requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,默认的选择。
- Supports:支持当前事务,如果没有当前事务,就以非事务方法执行。
- Mandatory:使用当前事务,如果没有当前事务,就抛出异常。
- NotSupported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
- Never:以非事务方式执行操作,如果当前事务存在则抛出异常。
- Nested:以嵌套事务方式执行。
第一步:配置 Startup.cs 注入
public void ConfigureServices(IServiceCollection services)
{services.AddSingleton<IFreeSql>(fsql);services.AddScoped<UnitOfWorkManager>();services.AddFreeRepository(null, typeof(Startup).Assembly);
}
UnitOfWorkManager 成员 | 说明 |
---|---|
IUnitOfWork Current | 返回当前的工作单元 |
void Binding(repository) | 将仓储的事务交给它管理 |
IUnitOfWork Begin(propagation, isolationLevel) | 创建工作单元 |
第二步:定义事务特性
[AttributeUsage(AttributeTargets.Method)]
public class TransactionalAttribute : Attribute
{/// <summary>/// 事务传播方式/// </summary>public Propagation Propagation { get; set; } = Propagation.Requierd;/// <summary>/// 事务隔离级别/// </summary>public IsolationLevel? IsolationLevel { get; set; }
}
第三步:引入动态代理库
在 Before 从容器中获取 UnitOfWorkManager,调用它的 var uow = Begin(attr.Propagation, attr.IsolationLevel) 方法
在 After 调用 Before 中的 uow.Commit 或者 Rollback 方法,最后调用 uow.Dispose
提醒:动态代理,一定注意处理好异步 await,否则会出现事务异常的问题
第四步:在 Controller 或者 Service 中使用事务特性
public class TopicService
{IBaseRepository<Topic> _repoTopic;IBaseRepository<Detail> _repoDetail;public TopicService(IBaseRepository<Topic> repoTopic, IBaseRepository<Detail> repoDetail){_repoTopic = repoTopic;_repoDetail = repoDetail;}[Transactional]public virtual void Test1(){//这里 _repoTopic、_repoDetail 所有 CRUD 操作都是一个工作单元this.Test2();}[Transactional(Propagation = Propagation.Nested)]public virtual void Test2() //嵌套事务,新的(不使用 Test1 的事务){//这里 _repoTopic、_repoDetail 所有 CRUD 操作都是一个工作单元}
}
是不是进方法就开事务呢?
不一定是真实事务,有可能是虚的,就是一个假的 unitofwork(不带事务)
也有可能是延用上一次的事务
也有可能是新开事务,具体要看传播模式
示例项目:https://github.com/dotnetcore/FreeSql/tree/master/Examples/aspnetcore_transaction
Autofac 动态代理参考项目:
- https://github.com/luoyunchong/lin-cms-dotnetcore
- https://github.com/luoyunchong/dotnetcore-examples/tree/master/ORM/FreeSql/OvOv.FreeSql.AutoFac.DynamicProxy
- AOP + FreeSql 跨方法异步事务 https://www.cnblogs.com/igeekfan/p/aop-freesql-autofac.html
13 手工分表
FreeSql 原生用法、FreeSql.Repository 仓储用法 都提供了 AsTable 方法对分表进行 CRUD 操作,例如:
var repo = fsql.GetRepository<Log>();
repo.AsTable(oldname => $"{oldname}_201903"); //对 Log_201903 表 CRUDrepo.Insert(new Log { ... });
跨库,但是在同一个数据库服务器下,也可以使用 AsTable(oldname => $“db2.dbo.{oldname}”)
//跨表查询
var sql = fsql.Select<User>().AsTable((type, oldname) => "table_1").AsTable((type, oldname) => "table_2").ToSql(a => a.Id);//select * from (SELECT a."Id" as1 FROM "table_1" a) ftb
//UNION ALL
//select * from (SELECT a."Id" as1 FROM "table_2" a) ftb
分表总结:
- 分表、相同服务器跨库 可以使用 AsTable 进行 CRUD;
- AsTable CodeFirst 会自动创建不存在的分表;
- 不可在分表分库的实体类型中使用《延时加载》;
v3.2.500 按时间自动分表方案:https://github.com/dotnetcore/FreeSql/discussions/1066
写在最后
FreeSql 他是免费自由的 ORM,也可以说是宝藏 ORM。更多文档请前往 github wiki 查看。
FreeSql 已经步入第四个年头,期待少年的你十年后还能归来在此贴回复,兑现当初吹过十年不变的承诺。
多的不说了,希望民间的开源力量越来越强大。
希望作者的努力能打动到你,请求正在使用的、善良的您能动一动小手指,把文章转发一下,让更多人知道 .NET 有这样一个好用的 ORM 存在。谢谢!!
FreeSql 使用最宽松的开源协议 MIT https://github.com/dotnetcore/FreeSql ,完全可以商用,文档齐全,甚至拿去卖钱也可以。QQ群:4336577(已满)、8578575(在线)、52508226(在线)
.NET ORM 仓储层必备的功能介绍之 FreeSql Repository 实现篇相关推荐
- Calico BGP功能介绍:BIRD简介
Calico 作为一种常用的 Kubernetes 网络插件,使用 BGP 协议对各节点的容器网络进行路由交换.本文是<Calico BGP 功能介绍>系列的第一篇,介绍 Calico 所 ...
- HEGERLS供应防腐防静电仓储层板货架 多层多功能自由组合仓库货架
重型仓储层板式货架,主要是由立柱.横梁.层板(托盘)组成,其立柱表面采用独特的菱形孔设计,同时安装时不用螺丝和焊接,均采用插接式组合,直接将横梁上的挂件往菱形孔内下压即可. 重型仓储层板式货架,还有多 ...
- 锂电池保护板中的MOS管作用与必备功能介绍
人们对锂电池的需求也越来越高,但是对锂电池保护板却不知道这是个什么东西,更不要说锂电池保护板里最主要的元器件了,今天小编就来说一下锂电池保护板元器件最主要是IC与MOS.IC先放着下次再说,现在来说下 ...
- OSS.Core基于Dapper封装(表达式解析+Emit)仓储层的构思及实现
最近趁着不忙,在构思一个搭建一个开源的完整项目,至于原因以及整个项目框架后边文章我再说明.既然要起一个完整的项目,那么数据仓储访问就必不可少,这篇文章我主要介绍这个新项目(OSS.Core)中我对仓储 ...
- setsockopt()函数功能介绍
setsockopt()函数功能介绍 功能描述:获取或者设置与某个套接字关联的选项.选项可能存在于多层协议中,它们总会出现在最上面的套接字层. 用法: #include <sys/types.h ...
- EFCore+Mysql仓储层建设(分页、多字段排序、部分字段更新)
前沿 园子里已有挺多博文介绍了EFCore+Mysql/MSSql如何进行使用,但实际开发不会把EF层放在Web层混合起来,需要多个项目配合结构清晰的进行分层工作,本文根据个人实践经验总结将各个项目进 ...
- Java SE 8新功能介绍:Lambda的遍历,过滤,处理集合和方法增强
在" Java SE 8新功能导览"系列的这篇文章中,我们将深入解释并探索代码,以了解如何使用lambda表达式和方法引用 遍历集合 ,并使用谓词接口过滤它们,实现默认方法在接口中 ...
- 使用inetaddress测试目标可达性_PDPS软件机器人虚拟仿真:Smart Place功能介绍与使用方法...
概述 对于机器人工作站或生产线的虚拟仿真,很大一部分的作用是找出机器人与工装夹具等外围设备的最佳布局位置.市面上大多数的工业机器人虚拟仿真软件都有这种专门用于检测机器人与外围设备之间最佳布局位置的功能 ...
- 带你走近AngularJS - 基本功能介绍
带你走近AngularJS系列: 带你走近AngularJS - 基本功能介绍 带你走近AngularJS - 体验指令实例 带你走近AngularJS - 创建自定义指令 ------------- ...
最新文章
- 文凭-决定的人生成败?下
- Synchronized及其实现原理
- mysql linux 中文乱码_解决MySQL中文乱码的问题
- Java注解--Java深度历险(转)
- Python 日期 的 加减 等 操作
- C语言空指针NULL详解
- 人生永无止境的意思是什么_人生追求永无止境名言
- 简书吐槽大会|山东被曝大量不合格疫苗
- 【ArcGIS|空间分析|网络分析】0 网络分析总结
- 204.计数质数 (力扣leetcode) 博主可答疑该问题
- 关于c语言从入门到精通那些事
- 计算机操作系统出现死锁的原因
- PS抠印章|证件照换背景
- 由pushViewController说起可能出线的各种死法
- 微信小程序搜索功能系列 一套全
- Unity VR开发教程 OpenXR+XR Interaction Toolkit (六)手与物品交互(触摸、抓取)
- 华硕VIVO BOOK15s啃苹果之路
- 大数据分析在病毒疫苗研究中的应用
- Carsim/Matlab/Simulink之ABS仿真模型搭建
- wireshark抓包vx ip
热门文章
- ORA-01400: cannot insert NULL into | 通过SQL链接服务器 往Oracle库的表中 插入默认值问题
- 如何自出版一本书:一份资源清单
- 机器人学笔记之——空间描述和变换:姿态的其他描述方法
- 游戏音乐制作中需要使用那些软件?
- 【大厂智力题】64匹马,8个赛道,找出前4名最少比赛多少场?
- 合同和协议的区别_合同的内容包括哪些,合同和协议的区别
- YOLOv5和YOLOv7环境(GPU)搭建测试成功
- QT 交叉编译 ARM / CSKY
- idea无法识别中文
- jlink怎么调试linux程序_Ubuntu12.10 使用JLink连接开发板用arm-gdb调试ARM程序