码迷,mamicode.com
首页 > 其他好文 > 详细

Entity Framework Core 2.0 入门简介

时间:2018-03-12 11:07:01      阅读:974      评论:0      收藏:0      [点我收藏+]

标签:技术   host   多对多   还需要   eager   鼠标右键   builder   最新   company   

不多说废话了, 直接切入正题.

EF Core支持情况

技术分享图片

EF Core的数据库Providers:

技术分享图片

此外还即将支持CosmosDB和 Oracle.

EFCore 2.0新的东西:

查询:

  • EF.Functions.Like()
  • Linq解释器的改进
  • 全局过滤(按类型)
  • 编译查询(Explicitly compiled query)
  • GroupJoin的SQL优化.

 映射:

  • Type Configuration 配置
  • Owned Entities (替代EF6的复杂类型)
  • Scalar UDF映射
  • 分表

性能和其他

  • DbContext Pooling, 这个很好
  • Raw SQL插入字符串.
  • Logging
  • 更容易定制配置

1.创建数据库和Model

准备.net core项目

项目结构如图:

技术分享图片

由于我使用的是VSCode, 所以需要使用命令行:

mkdir LearnEf && cd LearnEf
dotnet new sln // 创建解决方案

mkdir LearnEf.Domains && cd LearnEf.Domains
dotnet new classlib // 创建LearnEf.Domains项目

cd ..
mkdir LearnEf.Data && cd LearnEf.Data
dotnet new classlib // 创建LearnEf.Data项目

cd ..
mkdir LearnEf.UI && cd LearnEf.UI
dotnet new console // 创建控制台项目

cd ..
mkdir LearnEf.Tests && cd LearnEf.Tests
dotnet new xunit // 创建测试项目

为解决方案添加项目:

dotnet sln add LearnEf.UI/LearnEf.UI.csproj
dotnet sln add LearnEf.Domains/LearnEf.Domains.csproj
dotnet sln add LearnEf.Data/LearnEf.Data.csproj
dotnet sln add LearnEf.Tests/LearnEf.Tests.csproj

 

为项目之间添加引用:

LearnEf.Data依赖LearnEf.Domains:

cd LearnEf.Data
dotnet add reference ../LearnEf.Domains/LearnEf.Domains.csproj

 

LearnEf.Console依赖LearnEf.Domains和LearnEf.Data:

cd ../LearnEf.UI
dotnet add reference ../LearnEf.Domains/LearnEf.Domains.csproj ../LearnEf.Data/LearnEf.Data.csproj

 

LearnEf.Test依赖其它三个项目:

cd ../LearnEf.Tests
dotnet add reference ../LearnEf.Domains/LearnEf.Domains.csproj ../LearnEf.Data/LearnEf.Data.csproj ../LearnEf.UI/LearnEf.UI.csproj

 

(可能需要执行dotnet restore)

在Domains项目下直接建立两个Model, 典型的一对多关系Company和Department:

using System;
using System.Collections.Generic;

namespace LearnEf.Domains
{
    public class Company
    {
        public Company()
        {
            Departments = new List<Department>();
        }

        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime StartDate { get; set; }
        public List<Department> Departments { get; set; }
    }
}
namespace LearnEf.Domains
{
    public class Department
    {
        public int Id { get; set; }
        public int CompanyId { get; set; }
        public Company Company { get; set; }
    }
}

 

添加Entity Framework Core库:

首先Data项目肯定需要安装这个库, 而我要使用sql server, 参照官方文档, 直接在解决方案下执行这个命令:

dotnet add ./LearnEf.Data package Microsoft.EntityFrameworkCore.SqlServer
dotnet restore

 

创建DbContext:

在Data项目下创建MyContext.cs:

using LearnEf.Domains;
using Microsoft.EntityFrameworkCore;

namespace LearnEf.Data
{
    public class MyContext : DbContext
    {
        public DbSet<Company> Companies { get; set; }
        public DbSet<Department> Departments { get; set; }
    }
}

指定数据库Provider和Connection String:

在EFCore里, 必须明确指定Data Provider和Connection String.

可以在Context里面override这个Onconfiguring方法:

技术分享图片

有一个错误, 应该是Server=localhost;

(这里无需调用父类的方法, 因为父类的方法什么也没做).

UseSqlServer表示使用Sql Server作为Data Provider. 其参数就是Connection String.

在运行时EfCore第一次实例化MyContext的时候, 就会触发这个OnConfiguring方法. 此外, Efcore的迁移Api也可以获得该方法内的信息.

EF Core迁移:

简单的来说就是 Model变化 --> 创建migration文件 --> 应用Migration到数据库或生成执行脚本.

添加Migration (迁移):

由于我使用的是VSCode+dotnet cli的方法, 所以需要额外的步骤来使dotnet ef命令可用.

可以先试一下现在的效果:

技术分享图片

可以看到, dotnet ef 命令还不可用.

所以参考官方文档: https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/dotnet

可执行项目(Startup project)需要EFCore迁移引擎库, 所以对LearnEf.UI添加这个库:

dotnet add ./LearnEf.UI package Microsoft.EntityFrameworkCore.Design
dotnet restore

 

然后打开LearnEf.UI.csproj 添加这段代码, 这个库是EF的命令库:

 <ItemGroup>
    <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
  </ItemGroup>

 

最后内容如下:

技术分享图片

然后再执行dotnet ef命令, 就应该可用了:

技术分享图片

现在, 添加第一个迁移:

cd LearnEf.UI
dotnet ef migrations add Initial --project=../LearnEf.Data

 

--project参数是表示需要使用的项目是哪个.

命令执行后, 可以看到Data项目生成了Migrations目录和一套迁移文件和一个快照文件:

技术分享图片

检查这个Migration.

技术分享图片

前边带时间戳的那两个文件是迁移文件.

另一个是快照文件, EFCore Migrations用它来跟踪所有Models的当前状态. 这个文件非常重要, 因为下次你添加迁移的时候, EFcore将会读取这个快照并将它和Model的最新版本做比较, 就这样它就知道哪些地方需要有变化.

这个快照文件解决了老版本Entity Framework的一个顽固的团队问题.

使用迁移文件创建脚本或直接生成数据库.

生成创建数据库的SQL脚本:

dotnet ef migrations script --project=../LearnEf.Data/LearnEf.Data.csproj

 

技术分享图片

Sql脚本直接打印在了Command Prompt里面. 也可以通过指定--output参数来输出到具体的文件.

这里, 常规的做法是, 针对开发时的数据库, 可以通过命令直接创建和更新数据库. 而针对生产环境, 最好是生成sql脚本, 然后由相关人员去执行这个脚本来完成数据库的创建或者更新.

直接创建数据库:

dotnet ef database update --project=../LearnEf.Data/LearnEf.Data.csproj --verbose

 

--verbose表示显示执行的详细过程, 其结果差不多这样:

技术分享图片

这里的执行过程和逻辑是这样的: 如果数据库不存在, 那么efcore会在指定的连接字符串的地方建立该数据库, 并应用当前的迁移. 如果是生成的sql脚本的话, 那么这些动作必须由您自己来完成.

然后查看一下生成的表. 

不过首先, 如果您也和我一样, 没有装Sql server management studio或者 Visual Studio的话, 请您先安装VSCode的mssql这个扩展:

技术分享图片

重启后, 建立一个Sql文件夹, 然后建立一个Tables.sql文件, 打开命令面板(windows: Shift+Ctrl+P, mac: Cmd+Shift+P), 选择MS SQL: Connect.

技术分享图片

然后选择Create Connection Profile:

技术分享图片

输入Sql的服务器地址:

技术分享图片

再输入数据库名字:

技术分享图片

选择Sql Login(我使用的是Docker, 如果windows的话, 可能使用Integrated也可以):

技术分享图片

输入用户名:

技术分享图片

密码:

技术分享图片

选择是否保存密码:

技术分享图片

最后输入档案的名字:

技术分享图片

随后VSCode将尝试连接该数据库, 成功后右下角会这样显示 (我这里输入有一个错误, 数据库名字应该是LearnEF):

技术分享图片

随后在该文件中输入下面这个sql语句来查询所有的Table:

--  Table 列表
SELECT * FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE=BASE TABLE;

 

执行sql的快捷键是windows: Shift+Ctrp+E, mac: Cmd+Shift+E, 或者鼠标右键.

结果如图:

技术分享图片

OK表是创建成功了(还有一个迁移历史表, 这个您应该知道).

接下来我看看表的定义:

-- Companies表:
exec sp_help Companies;

 

技术分享图片

其中Name字段是可空的并且长度是-1也就是nvarchar(Max).

Departments表的Name字段也是一样的.

再看看那个MigrationHistory表:

-- MigrationHistory:
SELECT * FROM dbo.__EFMigrationsHistory;

技术分享图片

可以看到, efcore到migration 历史表里面只保存了MigrationId.

在老版本到ef里, migration历史表里面还保存着当时到迁移的快照, 创建迁移的时候还需要与数据库打交道. 这就是我上面提到的如果团队使用ef和源码管理的话, 就会遇到这个非常令人头疼的问题.

如果使用asp.net core的话.

在解决方案里再建立一个asp.net core mvc项目:

mkdir LearnEf.Web && cd LearnEf.Web
dotnet new mvc

 

在解决方案里添加该项目:

dotnet sln add ./LearnEf.Web/LearnEf.Web.csproj

 

为该项目添加必要的引用:

cd LearnEf.Web
dotnet add reference ../LearnEf.Domains/LearnEf.Domains.csproj ../LearnEf.Data/LearnEf.Data.csproj

 

为测试项目添加该项目引用:

cd ../*Tests
dotnet add reference ../LearnEf.Web/LearnEf.Web.csproj

 

操作完之后, 我们可以做以下调整, 去掉MyContext里面的OnConfiguring方法, 因为asp.net core有内置的依赖注入机制, 我可以把已经构建好的DbContextOptions直接注入到构造函数里:

这样的话, 我们可以让asp.net core来决定到底使用哪个Data Provider和Connection String:

技术分享图片

这也就意味着, Web项目需要引用EfCore和Sql Provider等, 但是不需要, 因为asp.net core 2.0这个项目模版引用了AspNetCore.All这个megapack, 里面都有这些东西了.

技术分享图片

虽然这个包什么都有, 也就是说很大, 但是如果您使用Visual Studio Tooling去部署的话, 那么它只会部署那些项目真正用到的包, 并不是所有的包.

接下来, 在Web项目的Startup添加EfCore相关的配置:

 public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.AddDbContext<MyContext>
                (options => options.UseSqlServer("Server=localhost; Database=LearnEf; User Id=sa; Password=Bx@steel1;"));
        }

 

这句话就是把MyContext注册到了asp.net core的服务容器中, 可以供注入, 同时在这里指定了Data Provider和Connection String.

与其把Connection String写死在这里, 不如使用appSettings.json文件:

技术分享图片

然后使用内置的方法读取该Connection String:

 public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.AddDbContext<MyContext>
                (options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
        }

 

回到命令行进入Web项目, 使用dotnet ef命令:

技术分享图片

说明需要添加上面提到的库, 这里就不重复了.

然后, 手动添加一个Migration叫做InitialAspNetCore:

dotnet ef migrations add InitialAspNetCore --project=../LearnEf.Data

 

看一下迁移文件:

技术分享图片

是空的, 因为我之前已经使用UI那个项目进行过迁移更新了. 所以我要把这个迁移删掉:

dotnet ef migrations remove --project=../LearnEf.Data

 

然后这两个迁移文件就删掉了:

技术分享图片

多对多关系和一对一关系:

这部分的官方文档在这: https://docs.microsoft.com/en-us/ef/core/modeling/relationships

对于多对多关系, efcore需要使用一个中间表, 我想基本ef使用者都知道这个了, 我就直接贴代码吧.

建立一个City.cs:

namespace LearnEf.Domains
{
    public class City
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

 

Company和City是多对多的关系, 所以需要建立一个中间表,叫做 CompanyCity:

namespace LearnEf.Domains
{
    public class CompanyCity
    {
        public int CompanyId { get; set; }
        public int CityId { get; set; }
        public Company Company { get; set; }
        public City City { get; set; }
    }
}

 

修改Company:

技术分享图片

修改City:

技术分享图片

尽管Efcore可以推断出来这个多对多关系, 但是我还是使用一下FluentApi来自定义配置一下这个表的主键:

MyContext.cs:

using LearnEf.Domains;
using Microsoft.EntityFrameworkCore;

namespace LearnEf.Data
{
    public class MyContext : DbContext
    {
        public MyContext(DbContextOptions<MyContext> options)
            : base(options)
        {

        }
        public DbSet<Company> Companies { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<CompanyCity> CompanyCities { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<CompanyCity>()
                .HasKey(c => new { c.CompanyId, c.CityId });
        }

        // protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        // {
        //     optionsBuilder.UseSqlServer("Server=localhost; Database=LearnEf; User Id=sa; Password=Bx@steel1;");
        //     base.OnConfiguring(optionsBuilder);
        // }
    }
}

 

完整的写法应该是:

技术分享图片

其中红框里面的部分不写也行.

接下来建立一个一对一关系, 创建Model叫Owner.cs:

namespace LearnEf.Domains
{
    public class Owner
    {
public int Id { get; set;}
public int CompanyId { get; set; } public string Name { get; set; } public Company Company { get; set; } } }

 

修改Company:

技术分享图片

配置关系:

protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<CompanyCity>()
                .HasKey(c => new { c.CompanyId, c.CityId });

            modelBuilder.Entity<CompanyCity>().HasOne(x => x.Company)
                .WithMany(x => x.CompanyCities).HasForeignKey(x => x.CompanyId);

            modelBuilder.Entity<CompanyCity>().HasOne(x => x.City)
                .WithMany(x => x.CompanyCities).HasForeignKey(x => x.CityId);

            modelBuilder.Entity<Owner>().HasOne(x => x.Company).WithOne(x => x.Owner)
                .HasForeignKey<Owner>(x => x.CompanyId);
        }

 

 

这里面呢, 这个Owner对于Company 来说 是可空的. 而对于Owner来说, Company是必须的. 如果针对Owner想让Company是可空的, 那么CompanyId的类型就应该设置成int?.

再添加一个迁移:

dotnet ef migrations add AddRelationships --project=../LearnEf.Data

 

查看迁移文件:

技术分享图片

查看一下快照;

技术分享图片

没问题, 那么更新数据库:

dotnet ef database update AddRelationships --project=../LearnEf.Data --verbose

 

更新成功:

技术分享图片

对现有数据库的反向工程

这部分请查看官方文档吧, 很简单, 我实验了几次, 但是目前还没有这个需求.

使用Model与数据库交互

输出Sql语句.

对于asp.net core 2.0项目, 参考官方文档: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?tabs=aspnetcore2x

实际上, 项目已经配置好Logging部分了, 默认是打印到控制台和Debug窗口的. 源码: https://github.com/aspnet/MetaPackages/blob/dev/src/Microsoft.AspNetCore/WebHost.cs

而对于console项目, 文档在这: https://docs.microsoft.com/en-us/ef/core/miscellaneous/logging

需要对LearnEf.Data项目添加这个包: 

cd LearnEf.Data
dotnet add package Microsoft.Extensions.Logging.Console
dotnet restore

 

然后为了使用console项目, 需要把MyContext改回来:

技术分享图片

这部分首先是使用LoggerFactory创建了一个特殊的Console Logger. .net core的logging可以显示很多的信息, 这里我放置了两个过滤: 第一个表示只显示Sql命令, 第二个表示细节的显示程度是Information级别.

最后还要在OnConfiguring方法里告诉modelBuilder使用MyLoggerFactory作为LoggerFactory.

这就配置好了.

插入数据.

这部分很简单, 打开UI项目的Program.cs:

技术分享图片

这里都懂的, 创建好model之后, 添加到context的DbSet属性里, 这时context就开始追踪这个model了.

SaveChanges方法, 会检查所有被追踪的models, 读取他们的状态. 这里用到是Add方法, context就会知道这个model的状态是new, 所以就应该被插入到数据库. 然后它就根据配置会生成出相应的sql语句, 然后把这个SQL语句执行到数据库. 如果有返回数据的话, 就取得该数据.

下面就运行一下这个console程序:

dotnet run --project=./LearnEf.UI

 

看下控制台:

技术分享图片

可以看到输出了sql语句, 而且这个出入动作后, 做了一个查询把插入数据生成的Id取了回来.

默认情况下log不显示传进去的参数, 这是为了安全. 但是可以通过修改配置来显示参数:

技术分享图片

然后控制台就会显示这些参数了:

技术分享图片

批量插入操作.

技术分享图片

可以使用AddRange添加多条数据. 其参数可以是params或者集合.

可以看到这个和之前Add的Sql语句是完全不同的:

技术分享图片

这个语句我不是很明白.

批量添加不同类型的数据:

技术分享图片

使用context的AddRange或Add方法, DbContext可以推断出参数的类型, 并执行正确的操作. 上面的方法就是使用了DbContext.AddRange方法, 一次性添加了两种不同类型的model.

这两个方法对于写一些通用方法或者处理复杂的情况是很有用的.

Sql Server对于批量操作的限制是, 一次只能最多处理1000个SQL命令, 多出来的命令将会分批执行.

如果想更改这个限制, 可以这样配置参数:

技术分享图片

简单查询.

针对DbSet, 使用Linq的ToList方法, 会触发对数据库对查询操作:

首先把Company的ToString方法写上:

技术分享图片

这样方便输入到控制台.

然后写查询方法:

技术分享图片

看结果:

技术分享图片

EfCore到查询有两类语法, 一种是Linq方法, 另一种是Linq查询语法:

这种是Linq方法:

技术分享图片

下面这种是Linq查询语法:

技术分享图片

我基本都是使用第一种方法.

除了ToList(Async)可以触发查询以外, 遍历foreach也可以触发查询:

技术分享图片

但是这种情况下, 可能会有性能问题. 因为:

在遍历开始的时候, 数据库连接打开, 并且会一直保持打开的状态, 直到遍历结束.

所以如果这个遍历很耗时, 那么可能会发生一些问题.

最好的办法还是首先执行ToList, 然后再遍历.

查询的过滤.

这部分和以前的EF基本没啥变化.

技术分享图片

这个很简单, 不说了.

这里列一下可触发查询的Linq方法:

技术分享图片

还有个两个方法是DbSet的方法, 也可以触发查询动作:

技术分享图片

上面这些方法都应该很熟悉, 我就不写了.

过滤的条件可以直接家在上面的某些方法里面, 例如:

技术分享图片

通过主键查询, 就可以用DbSet的Find方法:

技术分享图片

这个方法有个优点, 就是如果这条数据已经在Context里面追踪了, 那么查询的时候就不查数据库了, 直接会返回内存中的数据.

EF.Functions.Like 这个方法是新方法, 就像是Sql语句里面的Like一样, 或者字符串的Contains方法:

技术分享图片

这个感觉更像Sql语句, 输出到Console的Sql语句如下:

技术分享图片

这里还要谈的是First/FirstOrDefault/Last/LastOrDefaut方法.

使用这些方法必须先使用OrderBy/OrderByDescending排序. 虽然不使用的话也不会报错, 但是, 整个过程就会变成这样, context把整个表的数据家在到内存里, 然后返回第一条/最后一条数据. 如果表的数据比较多的话, 那么就会有性能问题了.

更新数据.

技术分享图片

很简单, context所追踪的model属性变化后, SaveChanges就会更新到数据库.

当然, 多个更新操作和插入等操作可以批量执行.

离线更新.

技术分享图片

就是这种情况, 新的context一开始并没有追踪one这个数据. 通过使用Update方法, 追踪并设置状态为update. 然后更新到数据库.

技术分享图片

可以看到, 在这种情况下, EfCore会更新该model到所有属性.

Update同样也有DbSet的UpdateRange方法, 也有context到Update和UpdateRange方法, 这点和Add是一样的.

还有一种方法用于更新, 这个以后再说.

删除数据.

DbContext只能删除它追踪的model.

技术分享图片

非常简单, 从log可以看到, 删除动作只用到了主键:

技术分享图片

如果是删除的离线model, 那么Remove方法首先会让Dbcontext追踪这个model, 然后设置状态为Deleted.

删除同样有RemoveRange方法.

Raw SQL查询/命令:

这部分请看文档:

命令: DbContext.Database.ExecuteSqlCommand();

技术分享图片

技术分享图片

查询: DbSet.FromSql() https://docs.microsoft.com/en-us/ef/core/querying/raw-sql;

这个方法目前还有一些限制, 它只能返回实体的类型, 并且得返回domain model所有的属性, 而且属性的名字必须也得一一对应. SQL语句不可以包含关联的导航属性, 但是可以配合Include使用以达到该效果(https://docs.microsoft.com/en-us/ef/core/querying/raw-sql#including-related-data).

技术分享图片

技术分享图片

更多的传递参数方式还需要看文档.

查询和保存关联数据.

插入关联数据.

我之前忘记在Department里面添加Name字段了, 现在添加一下, 具体过程就不写了.

插入关联数据有几种情况:

1.直接把要添加的Model的导航属性附上值就可以了, 这里的Department不需要写外键.

技术分享图片

看一下Sql:

技术分享图片

这个过程一共分两步: 1 插入主表, 2,使用刚插入主表数据的Id, 插入子表数据.

2.为数据库中的数据添加导航属性.

技术分享图片

这时, 因为该数据是被context追踪的, 所以只需在它的导航属性添加新记录, 然后保存即可.

3.离线数据添加导航属性.

这时候就必须使用外键了.

技术分享图片

预加载关联数据 Eager Loading.

也就是查询的时候一次性把数据和其导航属性的数据一同查询出来.

技术分享图片

看看SQL:

技术分享图片

这个过程是分两步实现的, 首先查询了主表, 然后再查询的子表. 这样做的好处就是性能提升.

(FromSql也可以Include).

预加载子表的子表:

可以使用ThenInclude方法, 这个可以老版本ef没有的.

技术分享图片

这里查询Department的时候, 将其关联表Company也查询了出来, 同时也把Company的关联表Owner也查询了出来.

查询中映射关联数据.

技术分享图片

使用Select可以返回匿名类, 里面可以自定义属性.

这个匿名类只在方法内有效.

看下SQL:

技术分享图片

可以看到SQL中只Select了匿名类里面需要的字段.

如果需要在方法外使用该结果, 那么可以使用dynamic, 或者建立一个对应的struct或者class.

使用关联导航属性过滤, 但是不加载它们.

技术分享图片

SQL:

技术分享图片

这个比较简单. 看sql一切就明白了.

修改关联数据.

也会分两种情况, 被追踪和离线数据.

被追踪的情况下比较简单, 直接修改关联数据的属性即可:

技术分享图片

看一下SQL:

技术分享图片

确实改了.

这种情况下, 删除关联数据库也很简单:

技术分享图片

看下SQL:

技术分享图片

删除了.

下面来看看离线状态下的操作.

技术分享图片

这里需要使用update, 把该数据添加到context的追踪范围内.

看一下SQL:

技术分享图片

这个就比较怪异了.

它update了该departmt和它的company以及company下的其他department和company的owner. 这些值倒是原来的值.

这是因为, 看上面的代码, 查询的时候department的关联属性company以及company下的departments和owner一同被加载了.

尽管我只update了一个department, 但是efcore把其他关联的数据都识别出来了.

从DbContext的ChangeTracker属性下的StateManger可以看到有多少个变化.

这一点非常的重要.

如何避免这个陷阱呢?

可以这样做: 直接设置dbContext.Entry().State的值

技术分享图片

这时, 再看看SQL:

技术分享图片

嗯. 没错, 只更新了需要更新的对象.

 

 

2.1版本将于2018年上半年发布, 请查看官网的路线图: https://github.com/aspnet/EntityFrameworkCore/wiki/roadmap

完. 

Entity Framework Core 2.0 入门简介

标签:技术   host   多对多   还需要   eager   鼠标右键   builder   最新   company   

原文地址:https://www.cnblogs.com/cgzl/p/8543772.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!