EF Core的学习之路01

本文章是我听B站杨中科的所做的笔记

杨中科B站视频链接:.NET 6教程,.Net Core 2022视频教程,杨中科主讲_哔哩哔哩_bilibili

什么时ORM

1、说明:本课程需要你有数据库、SQL等基础知识

2、ORM:Object Relational Mapping。让开发者用对象操作的形式操作关系数据库 比如插入:

User user = new User(){Name="admin",Password="123"};
orm.Save(user);

比如查询:

Book b = orm.Books.Single(b=>b.Id==3
        ||b.Name.Contains(".NET"));
string bookName = b.Name;
string aName = a.Author.Name;

3、有哪些ORM:EF Core、Dapper、SqlSugar、FreeSql等

EF Core与其他ORM比较

1、Entity Framework Core(EF Core)是微软官方ORM框架。优点:功能强大、官方支持、生产效率高、力求屏蔽数据库差异;缺点:复杂、上手门槛高、不熟悉EFCore的话可能会进坑。

2、Dapper。优点:简单、N分钟即可上手,行为可预期性强;缺点:生产效率低,需要处理底层数据库差异

3、EF Core是模型驱动的开发思想,Dapper是数据库驱动的开发思想的。没有优劣,只有比较。

4、性能:Dapper不等于性能高;EF Core不等于性能差。

5、EF Core是官方推荐、推进的框架,尽量屏蔽底层数据库差异,.NET开发者必须熟悉,根据的项目情况再决定用哪个。

选择

1、建议:对于后台系统、信息系统等和数据库相关开发工作量大的系统,且团队比较稳定,用EF Core;对于互联网系统等数据库相关工作量不大的系统,或者团队不稳定,用Dapper

2、在项目中可以混用,只要注意EF Core的缓存、Tracking等问题即可

EF Core与EF比较

1、EF有DB First、Model First、Code First。EF Core不支持模型优先,推荐使用代码优先,遗留系统可以使用ScafffoldDbContext来生成代码实现类似DBFirst的效果,但是推荐用Code First

2、EF会对实体上的标注做校验,EF Core追求轻量化,不校验。

3、熟悉EF的话,掌握EFCore会很容易,很多用法都移植过来了,EF Core又增加了很多新东西 4、EF中一些类的命名空间以及一些方法的名字再EF Core中稍有不同。

5、EF不再做新特性增加

搭建EF Core环境

用什么数据库

1、EF Core是对于底层ADO.NET Core的封装,因此ADO.NET Core支持的数据库不一定被EF Core支持

2、EF Core支持所有主流的数据库,包括MS SQL Server、Oracle、MySql、PostgreSql、Sqlite等。可以自己实现Provider支持其他数据库。国产数据库支持问题

3、对于SQLServer支持最完美,MySql、PostgreSQL也不错(有能解决的小坑)。这三者是.NET圈中用的最多的三个。EF Core能尽量屏蔽底层数据库差异

开发环境搭建

1、经典步骤:建实体类;建DbCotext;生成数据库;编写调用EF Core的业务代码

2、Boos.cs

public class Book
    {
        public long Id { get; set; }//主键
        public string Title { get; set; }//标题
        public DateTime PubTime { get; set; }//发布日期
        public double Price { get; set; }//单价
    }

3、Install-Package Microsoft.EntityFrameworkCore.SqlServer

4、创建实现了IEntityTypeConfiguration接口的实体配置类,配置实体类和数据库的对应关系

class BookEntityConfig : IEntityTypeConfiguration<Book>
{
    public void Configure(EntityTypeBuilder<Book> builder)
    {
        builder.ToTable("T_Books");
    }
}

5、创建继承自DbContext的类

class TestDbContext:DbContext
{
    public DbSet<Book> Books { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        string connStr = "Server=.;Database=demo1;Trusted_Connection=True;MultipleActiveResultSets=true";
        optionsBuilder.UseSqlServer(connStr);
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
    }
}

6、migration数据库迁移 面向对象的开发中,数据库不是程序员手动创建的,而是由Migration工具生成的。关系数据库只是存放模型数据的一个媒介而已,理想状态下,程序员不用关心数据库的操作。根据对象的定义变化,自动更新数据库的表以及表结构的操作,叫做Migration(迁移)。迁移可以分为多步(项目进化),也可以回滚。

7、为了使用生成数据库的工具,Nuget安装:Microsoft.EntityFrameworkCore.Tool,否则执行Add-Migration等命令会报错

8、再”程序包管理控制台“中执行如下命令Add-Migration InitialCreate,会自动再项目的Migration文件夹中生成操作数据库的C#操作。代码需要执行后才会应用对数据库的操作。”程序包管理器控制台“中执行Update-database。查看一下数据库,表建好了

9、修改表结构

1)项目开发中,根据需要,可能会在已有实体中修改、新增、删除表、列等

2)想要限制Title的最大长度为50,Title字段设置为”不可为空“,并且想增加一个不可为空且最大长度为20的AuthorName(作者名字)属性,首先子啊Book实体类中增加一个AuthorName属性

3)修改BookEntityConfig

builder.ToTable("T_Books");
builder.Property(e => e.Title).HasMaxLength(50).IsRequired();
builder.Property(e => e.AuthorName).HasMaxLength(20).IsRequired();

4)执行Add-Migration AddAuthorName_ModifyTitle。取名字由意义

5)执行Update-Database

EF Core增删改查

插入输入

1、只要操作Books属性,就可以向数据库中增加数据,但是通过C#代码修改Books中的数据只是修改内存中的数据。对Books做修改后,需要调用DbContext的异步方法SaveChange(),但是用EF Core都推荐用异步方法

2、EF Core默认会跟踪(Track)实体类对象以及DbSet的改变

查询数据

1、DbSet实现了IEnumberable<T>接口,因此可以对DbSet实施Linq操作来进行数据查询。EF Core会把Linq操作转换为SQL语句。面向对象,而不是面向数据库(SQL)

ctx.Books.Where(b => b.Price > 80)
Book b1 = ctx.Books.Single(b => b.Title== "零基础趣学C语言");
Book b2 = ctx.Books.FirstOrDefault(b=>b.Id==9);

可以使用OrderBy操作进行数据的排序

IEnumerable<Book> books = ctx.Books.OrderByDescending(b => b.Price);

GroupBy也可以

var groups = ctx.Books.GroupBy(b => b.AuthorName)
    .Select(g => new { AuthorName = g.Key, BooksCount = g.Count(), MaxPrice = g.Max(b => b.Price) });
foreach(var g in groups)
{
    Console.WriteLine($"作者名:{g.AuthorName},著作数量:{g.BooksCount},最贵的价格:{g.MaxPrice}");
}

大部分Linq操作都能作用与EF Core

修改、删除

1、要对数据进行修改,首先需要把要修改的数据查询出来,然后再对查询出来的对象进行修改,然后再执行SaveChangesAsync()保存修改

var b = ctx.Books.Single(b=>b.Title== "数学之美");
b.AuthorName = "Jun Wu";
await ctx.SaveChangesAsync();

2、删除也是先把要删除的数据查询出来,然后再调用DbSet或者DbContext的Remove方法把对象删除然后再执行SaveChangeAsync()保存修改。

var b = ctx.Books.Single(b => b.Title == "数学之美");
ctx.Remove(b);//也可以写成ctx.Books.Remove(b);
await ctx.SaveChangesAsync();

批量修改、删除

1、目前批量修改、删除多条数据的方法:局限性、性能低:查出来,再一条条Update、Delete,而不能执行Update ...where;delete ... where;

2、官方目前还没有支持高效的批量Update、Delete,有在后续版本中共增加,但是目前只是前期意见征询阶段

3、有一个开源的高效批量修改、删除的开源项目。Zack.EFCore.Batch GitHub - yangzhongke/Zack.EFCore.Batch: Deleting or Updating multiple records from a LINQ Query in a SQL statement without loading entities

EF Core实体配置

约定配置

主要规则:

1、表明采用DbContext中的对应的DbSet的属性名

2、数据表列的名字采用实体类属性名字,列的数据类型采用和实体类属性类型最兼容的类型

3、数据表列的可空性取决于对应实体类属性的可空性

4、名字为Id的属性为主键,如果主键为short,int或者long类型,则默认采用自增字段,如果主键为Guid类型,则默认采用默认的Guid生成机制生成主键值

两种配置方式

Data Annotation

1、把配置以特性(Annotation)的形式标注再实体类中。

[Table("T_Books")]
public class Book
{
}

优点:简单;缺点:耦合

2、Fluent API: builder.ToTable("T_Books"); 把配置写到单独的配置类中。缺点:复杂;优点:解耦

3、大部分功能重叠。可以混用,但是不建议混用

Fluent API

1、视图与实体类映射:modelBuilder.Entity<Blog>().ToView("blogsView");

2、排除属性映射:modelBuilder.Entity<Blog>().Property.Ignore(b=>b.Name2);

3、配置列名:modelBuilder.Entity<Blog>().Property(b=>b.BlogId).HasColumnName("blog_id") 4、配置列数据类型:builder.Property(e=>e.Title).HasColumnType("varchar(200)")

5、配置主键:默认把名字为Id或者“实体类型+Id”的属性作为主键,可以用HasKey()来配置其他属性作为主键。modelBuilder.Entity<Student>().HasKey(c=>c.Number);支持复合主键,但是不建议使用

6、生成列的值:modelBuilder.Entity<Student().Property(b=>b.Number).ValueGenerateOnAdd();

7、可以用HasDefaultValue()为属性设定默认值modelBuilder.Entity<Student>().Property(b=>b.Age).HasDefaultValue(6);

8、

a)索引:modelBuilder.Entity<Blog>().HasIndex(b=>b.Url);

b)复合索引:modelBuilder.Entity<Person>().HasIndex(p=>new{p.FirstName,p.LastName});

c)唯一索引:IsUnique(); d)聚集索引:InClustered();

9、使用EF Core高级特性的时候谨慎,尽量不要和业务逻辑混合在一起,以免“不能自拔”。比如Ignore、Shadow、Table Splitting等。。。。

10、Fluent API 众多方法 Fluent API中很多方法都有多个重载。比如HasIndex、Property()。把Number属性定义为索引,下面两种方法都可以:

builder.HasIndex("Number");
builder.HasIndex(b=>b.Number);

推荐使用HasIndex(b=>b.Number).Property(b=>b.Number)这样的写法,因为这样利用的是C#的强类型检查机制

11、选择

a)Data Annotation、Fluent API大部分功能重叠。可以混用,但是不建议混用

b)有人建议混用,即用了Data Annotation的简单,又用到Fluent API的强大,而且实体类标注的【MaxLength(50)】、【Required】等标注可以被ASP.NET Core中的验证框架等复用。建议混用

c)很多人都倾向只使用Fluent API。主要讲解Fluent API为主(尽量用约定),如果项目强制用Data Annotation请翻文档,知识都是通用的

主键不是小事

自增主键

1、EF Core支持多种主键生成策略:自增增长;Guid;Hi/Lo算法等

2、自动增长。优点:简单;缺点:数据库迁移以及分布式系统中比较麻烦;并发性能差。long、int等类型主键,默认是自增。因为是数据库生成的值,所以SaveChanges后会自动把主键的值更新到Id属性。试验一下。场景:插入帖子后,自动重定向帖子地址。

3、自增字段的代码中不能为Id赋值,必须保持默认值0,否则运行的时候就会报错

Guid主键

1、Guid算法(或UUID算法)生成一个全局唯一的Id。适合于分布式系统,在进行多数据库数据合并的时候很简单。优点:简单,高并发,全局唯一;缺点:磁盘空间占用大

2、Guid值不连续。使用Guid类型做主键的时候,不能把主键设置为聚集索引。因为聚集索引是按照顺序保存主键的,因此用Guid做主键性能差。比如MySql的InnoDB引擎中主键是强制使用聚集索引的。有的数据支持部分的连续Guid,比如SQLServer中的NewSequentialId(),但也不能解决问题,在SQLServer等中,不要把Guid主键设置为聚集索引;在MySql中,插入频繁的表不要用Guid做主键

3、Guid既可以让EF Core给赋值,也可以手动赋值(推荐)

其他方案

1、混合自增和Guid(非复合主键)。用自增列做物理的主键,而用Guid列做逻辑上的主键。把自增列设置为表的主键,而在业务上查询数据的时候把Guid当主键用。在和其他表关联以及和外部系统通讯的时候(比如前端显示数据的标识的时候)都是使用Guid列。不仅保证了性能,而且利用了Guid的优点,而且减轻了主键自增性导致主键值可被预测带来的安全性问题

2、Hi/Lo算法:EF Core支持Hi/Lo算法来优化自增列。子增值有两部分组成:高位(Hi)和低位(Lo),高位由数据库生成,两个高位之间间隔若干个值,由程序在本地生成低位,低位的值在本地自增生成。不同进程或者集群中不同服务器获取的Hi值不会重复,而本地进程计算的Lo则可以保证可以在本地高效率的生成主键值。但是HiLo算法不是EF Core的标准

Migrations

深入研究Mirations

1、使用迁移脚本,可以对当前连接的数据库执行编号更高的迁移,这个操作叫做“向上迁移”(Up),也可以执行把数据回退到旧的迁移,这个操作叫做“向下迁移”(Down)

2、除非由特殊情况,否则不要删除Migrations文件夹下的代码

3、进一步分析Migrations下的代码。分析Up、Down等方法。查看Migration编号

4、查看数据库的_EFMigrationsHistory表:记录当前数据库曾经应用过的迁移脚本,按顺序排列

Migrations其他命令

1、Update-Database XXX 把数据库回滚S到XXX的状态,迁移脚本不动。

2、Remove-Migration 删除最后一次的迁移脚本

3、Script-Migration 生成迁移SQL代码,有了Update-Database为什么还要生成SQL脚本。可以生成版本D到版本F的SQL脚本:Script-Migration D F;生成版本D到最新版本的SQL脚本:Script-Migration D

反向工程

1、根据数据库表来反向生成实体类

2、Scaffold-DbContext 'Server=.;Dabataba=demo;Trusted_Connection=True;MultipleActiveResultSets=true' Microsoft.EntityFrameworkCore.SqlServer

注意:

1、生成的实体类可能不能满足项目的需求,可能需要手工修改或者增加配置

2、再次运行反向工程工具,对文件所做的任何更改都将丢失

3、不建议把反向工具当成了日常开发工具使用,不建议DBFirst

EF Core底层如何操作数据库

传统的模式:

使用ORM框架模式:

EF Core模式。EF Core是把C#代码转换为SQL语句的框架:

查看生成的SQL语句

1、SQL Server Profiler查看SQLServer数据库当前执行的SQL语句

2、var book = ctx.Books.Where(b=>b.Price>10||b.Title.Contains("张"))

有哪些是EF Core是做不到的

why:

1、C#千变万化;SQL功能简单。存在合法的C#语句无法被翻译为SQL语句的情况

var books = ctx.Books.Where(b => IsOK(b.Title));
private static bool IsOK(string s)
{
    return s.Contains("张");
}
var books = ctx.Books.Where(b =>b.Title.PadLeft(5)=="hello");

不同数据的不同:

通过代码查看EF Core生成的SQL

标准日志:

//方法1
public static readonly ILoggerFactory MyLoggerFactory
    = LoggerFactory.Create(builder => { builder.AddConsole(); });
​
optionsBuilder.UseLoggerFactory(MyLoggerFactory);
​
//方法2 可以自己写代码过滤一些不需要的信息
optionsBuilder.LogTo(Console.WriteLine);

方法3:

1、上面两种方法无法直接得到一个操作的SQL语句,而且操作很多的情况下,容易混乱

2、EF Core的Where方法返回的是IQueryable类型,DbSet也实现了IQueryable接口。IQueryable有扩展方法ToQueryString()可以获得SQL

3、不需要真的执行查询才获取SQL语句;只能获取查询操作的

总结:写测试行代码,用简单日志;正式需要记录SQL给审核人员或者排查故障,用标准日志;开发阶段,从繁杂的查询操作中立即看到SQL,用ToQueryString()。

同样的Linq被翻译为不同的SQL语句

不同数据库方言不同

SqlServer:select top(3) * from t

MySql:select * from t limit 3

Oracle:select * from t where RowNum<=3 C#代码:

var books = ctx.Books.Where(b=>b.PubTime.Year>2010).Take(3);
foreach(var b in books)
{
    Console.WriteLine(b.Title);
}

同样的C#语句在不同数据库中被EF Core翻译成不同的SQL语句

因此迁移脚本不能跨数据库。通过给Add-Migration命令添加“-OutputDir”参数的形式来在同一个项目中为不同数据库生成不同的迁移脚本

MySQL项目中测试

1、EF Provider的选择:

2、Install-Package Pomelo.EntityFrameworkCore.MySql

3、optionBuilder.UseMySql("server=localhost;user=root;password=root;database=ef",new MySqlServerVersion(new Version(5,6,0)))

PostgreSQL项目中测试 。开源项目的主要贡献者是微软EF团队的主程序员,因此这个项目的可靠性还是比较高的。

1、Install-Package Npgsql.EntityFrameworkCore.PostgreSQL 2、optionBuilder.UseNpgsql("Host=127.0.0.1;Database=ef;Username=postgres;Password=123456")

EF Core一对多关系配置

什么是实体间关系

1、所谓“关系数据库”

2、复习:数据库表之间的关系:一对一、一对多、多对多

3、EF Core不仅支持单实体操作,更支持多实体的关系操作

4、三部曲:实体类中关系属性;FluentAPI关系配置;使用关系操作

一对多:实体类

1、文章实体类Article、评论实体类Comment。一篇文章对应多条评论

public class Article
{
    public long Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public List<Comment> Comments { get; set; } = new List<Comment>(); 
}
public class Comment
{
    public long Id { get; set; }
    public Article Article { get; set; }
    public string Message { get; set; }
}

一对多:关系配置

EF Core中实体之间关系的配置的套路:HasXXX(...).WithXXX(...);有XXX、反之带有XXX。XXX可选值One、Many 一对多:HasOne(...).WithMany(...); 一对一:HasOne(...).WithOne(...); 多对多:HasMany(...).WithMany(...);

一对多:关系配置

class ArticleConfig : IEntityTypeConfiguration<Article>
{
    public void Configure(EntityTypeBuilder<Article> builder)
    {
        builder.ToTable("T_Articles");
        builder.Property(a => a.Content).IsRequired().IsUnicode();
        builder.Property(a => a.Title).IsRequired().IsUnicode().HasMaxLength(255);
    }
}
class CommentConfig : IEntityTypeConfiguration<Comment>
{
    public void Configure(EntityTypeBuilder<Comment> builder)
    {
        builder.ToTable("T_Comments");
        builder.HasOne<Article>(c=>c.Article).WithMany(a => a.Comments).IsRequired();
        builder.Property(c=>c.Message).IsRequired().IsUnicode();
    }
}

一对多:实验

1、迁移生成数据库表

2、编写代码测试数据插入

3、不需要显式为Comment对象的Article属性赋值(当前赋值也不会出错),也不需要显式地把新创建的Comment类型的对象添加到DbContext。EF Core会“'顺竿爬'

Article a1 = new Article();
a1.Title = "微软发布.NET 6大版本的首个预览";
a1.Content = "微xxxx";
Comment c1 = new Comment() { Message="支持"};
Comment c2 = new Comment() { Message = "微软太牛了" };
Comment c3 = new Comment() { Message = "火钳刘明" };
a1.Comments.Add(c1);
a1.Comments.Add(c2);
a1.Comments.Add(c3);
using (TestDbContext ctx = new TestDbContext())
{
    ctx.Articles.Add(a1);
    await ctx.SaveChangesAsync();
}

一对多关系数据的获取

Article a = ctx.Articles.Include(a=>a.Comments).Single(a=>a.Id==1);
Console.WriteLine(a.Title);
foreach(Comment c in a.Comments)
{
    Console.WriteLine(c.Id+":"+c.Message);
}

Include定义在Microsoft.EntityFrameworkCore命名空间中。查看一下生成的SQL语句

如果没有Include,就没有关联,就查不到数据了

EF Core额外的外键字段

为什么需要外键属性?

1、EF Core会在数据库表中建外键列

2、如果需要获取外键列的值,就需要做关联查询,效率低

3、需要一种不需要Join直接获取外键列的值的方式

设置外键属性:

1、在实体类中显式声明一个外键属性

2、关系配置中通过HasForeignKey(c=>c.ArticleId)指定这个属性为外键

3、除非必要,否则不用声明,因为会引入重复

EF Core单项导航属性

配置方法:不设置反向的属性,然后配置的时候WithMany()不设置参数即可

单向属性如何反向获取数据:再查询一下即可:ctx.Leaves.Where(l=>l.Requester==u)

选择:对于主从结构的”一对多“表关系,一般是声明双向导航属性。而对于其他的”一对多“表关系,则用单项导航属性,否则可以自由决定是否用双向导航属性

关系配置在任何一方都可以

反着配置也可以:

//CommentConfig:
builder.HasOne<Article>(c=>c.Article).WithMany(a => a.Comments).IsRequired();
//ArticleConfig:
builder.HasMany<Comment>(a => a.Comments).WithOne(c => c.Article).IsRequired();

推荐策略:考虑到有单项导航属性的可能,我们一般用HasOne().WithMany()

自引用的组织结构树

代码:

class OrgUnit
{
    public long Id { get; set; }
    public string Name { get; set; }
    public OrgUnit Parent { get; set; }
    public List<OrgUnit> Children { get; set; } = new List<OrgUnit>();
}

配置:

builder.ToTable("T_OrgUnits");
builder.Property(o => o.Name).IsRequired().IsUnicode().HasMaxLength(100);
builder.HasOne<OrgUnit>(u => u.Parent).WithMany(p => p.Children);

测试:

1、测试数据插入

2、测试递归缩进打印:

static async Task Main(string[] args)
{
    using (TestDbContext ctx = new TestDbContext())
    {
        OrgUnit ouRoot = ctx.OrgUnits.Single(o=>o.Parent==null);
        Console.WriteLine(ouRoot.Name);
        PrintChildren(0, ctx, ouRoot);
    }
}
​
static void PrintChildren(int indentLevel, TestDbContext ctx,OrgUnit parent)
{
    //获取以parent为父节点的组织单元
    var children = ctx.OrgUnits.Include(o => o.Children).Where(o=>o.Parent==parent);
    indentLevel++;//缩进级别
    foreach (var ou in children)
    {
        Console.Write(new string('+',indentLevel));//输出缩进
        Console.WriteLine(ou.Name);
        PrintChildren(indentLevel, ctx, ou);
    }
}

EF Core 一对一

在“一对多”的关系中,很显然是需要在“多”端有一个指向“一”端的列,因此除非我们需要显式的声明一个外键属性,否则EF Core会自动在“多”端的表中生成一个指向“一”端的外键列,不需要我们显式的声明一个外键属性。对于一对一关系,由于双方是“平等”的关系,外键列可以建在任意一方,因此必须显式的在其中一个实体类中声明一个外键属性。代码:

class Order
{
    public long Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public Delivery Delivery { get; set;} 
}
class Delivery
{
    public long Id { get; set; }
    public string CompanyName { get; set; }
    public String Number { get; set; }
    public Order Order { get; set; }
    public long OrderId { get; set; }
}

配置:

builder.HasOne<Delivery>(o => o.Delivery).WithOne(d => d.Order).HasForeignKey<Delivery>(d=>d.OrderId);

EF Core 多对多关系

1、多对多:老师---学生

2、EF Core5.0开始,才正式支持多对多

3、需要中间表,举例数据

代码:

class Student
{
    public long Id { get; set; }
    public string Name { get; set; }
    public List<Teacher> Teachers { get; set; } = new List<Teacher>();
}
class Teacher
{
    public long Id { get; set; }
    public string Name { get; set; }
    public List<Student> Students { get; set; } = new List<Student>();
}

多对多的关系配置可以放到任何一方的配置类中,我这里把关系配置代码放到了Student类的配置中。配置:

builder.HasMany<Teacher>(s => s.Teachers).WithMany(t=>t.Students).UsingEntity(j=>j.ToTable("T_Students_Teachers"));

测试代码:

1、测试数据插入

2、查询一下所有老师,并且列出它们的学生

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值