Entity Framework 代码模板

280 篇文章 6 订阅

各种使用方式

EntityClient+EntitySQL

string city = "London";
using (EntityConnection cn = new EntityConnection("Name=Entities"))
{
  cn.Open();
  EntityCommand cmd = cn.CreateCommand();
  cmd.CommandText = @"SELECT VALUE c FROM Entities.Customers AS c WHERE 
                     c.Address.City = @city";
  cmd.Parameters.AddWithValue("city", city);
  DbDataReader rdr = cmd.ExecuteReader(CommandBehavior.SequentialAccess);
  while (rdr.Read())
    Console.WriteLine(rdr["CompanyName"].ToString());
  rdr.Close();
}

ObjectService+EntitySQL

string city = "London";
using (Entities entities = new Entities()) 
{
  ObjectQuery<Customers> query = entities.CreateQuery<Customers>(
    "SELECT VALUE c FROM Customers AS c WHERE c.Address.City = @city",
    new ObjectParameter("city", city)
  );

  foreach (Customers c in query)
    Console.WriteLine(c.CompanyName);
}

ObjectContext+LINQ( to Entity)
方式一:


string city = "London";
using (Entities entities = new Entities()) 
{
  var query = from c in entities.Customers
              where c.Address.City == city
              select c;

  foreach (Customers c in query)
    Console.WriteLine(c.CompanyName);
}

方式二:

string city = "London";
using (Entities entities = new Entities()) 
{
  var query = entities.Customers.Where(r => r.Address.City == city);

  foreach (Customers c in query)
    Console.WriteLine(c.CompanyName);
}

这两段示例代码中的entities.Customer的写法隐式调用了2中示例的ObjectQuery<Customers>来进行查
询(关于此可以参见EDM的设计器文件-xxx.designer.cs)。在方式二中的Where方法传入的是一个
Lambda表达式,你也可以传入一条EntitySQL语句做参数来将LINQ与EntitySQL结合使用。如下代码演示
其使用:

string city = "London";
using (Entities entities = new Entities()) 
{
  var query = entities.Customers.Where("r.Address.City = '"+city+"'");

  foreach (Customers c in query)
    Console.WriteLine(c.CompanyName);
}

下面代码演示使用EntitySQL的like完成模糊查询:

context.Customer.Where("it.CustomerID LIKE @CustomerID", new 

System.Data.Objects.ObjectParameter("CustomerID","%V%"));
这个并不是只能使用EntitySQL来实现,LINQ to Entity也可以很容易完成。如下代码:  


context.Customer.Where(r => r.CustomerID.Contains("V")); 
同理,"V%"、"%V"可以分别使用StartsWith()与EndsWith()函数实现。
 


    使用LINQ to Entity需要注意的一个方面是,在完成查询得到需要的结果后使用ToList或ToArray方


法将结果转变为内存中的对象,然后使用LINQ to Objects来处理,否则处在Entity Framework的联机模


式下对性能有很大的影响。
========

EdmGen工具



在.NET Framework 3.5的文件夹下有一个名为EdmGen的工具,Visual Studio的实体设计器就是调用这个


工具来完成EDM的生成等操作。通过直接使用这个工具的命令行选项我们可以进行更多的控制。


这个命令的参数及作用如下:


EdmGen 选项


/mode:EntityClassGeneration 从 csdl 文件生成对象


/mode:FromSsdlGeneration 从 ssdl 文件生成 msl、csdl 和对象


/mode:ValidateArtifacts 验证 ssdl、msl 和 csdl 文件


/mode:ViewGeneration 从 ssdl、msl 和 csdl 文件生成映射视图


/mode:FullGeneration 从数据库生成 ssdl、msl、csdl 和对象


/project:<字符串> 用于所有项目文件的基名称 (短格式: /p)


/provider:<字符串> 用于 ssdl 生成的 Ado.Net 数据提供程序的名称。(短格式: /prov)


/connectionstring:<连接字符串> 您要连接到的数据库的连接字符串 (短格式: /c)


/incsdl:<文件> 从中读取概念模型的文件


/refcsdl:<文件> 包含 /incsdl 文件所依赖的类型的 csdl 文件


/inmsl:<文件> 从中读取映射的文件


/inssdl:<文件> 从中读取存储模型的文件


/outcsdl:<文件> 将生成的概念模型写入到其中的文件


/outmsl:<文件> 将生成的映射写入到其中的文件


/outssdl:<文件> 将生成的存储模型写入到其中的文件


/outobjectlayer:<文件> 将生成的对象层写入到其中的文件


/outviews:<文件> 将预生成的视图对象写入到其中的文件


/language:CSharp 使用 C# 语言生成代码


/language:VB 使用 VB 语言生成代码


/namespace:<字符串> 用于概念模型类型的命名空间名称


/entitycontainer:<字符串> 用于概念模型中的 EntityContainer 的名称


/help 显示用法信息 (短格式: /?)


/nologo 取消显示版权消息


使用示例:


从 Northwind 示例数据库生成完整 Entity Model。


1 EdmGen /mode:FullGeneration /project:Northwind /provider:System.Data.SqlClient 


/connectionstring:"server=.\sqlexpress;integrated security=true; database=northwind"
从 ssdl 文件开始生成 Entity Model。


1 EdmGen /mode:FromSSDLGeneration /inssdl:Northwind.ssdl /project:Northwind
验证 Entity Model。


1 EdmGen /mode:ValidateArtifacts /inssdl:Northwind.ssdl /inmsl:Northwind.msl 


/incsdl:Northwind.csdl
 
========

含有Association的EDM的使用

    当前版本的Entity Framework不支持自动延迟加载,所有当前未使用的关系中的相关实体默认按不


加载处理,当我们需要通过关系获取一个实体对象时,我们可以采用两种方法:


显示加载
实体框架针对 EntityReference 类的每个实例提供一个 Load 方法。此方法可用于显式加载与另一实体


相关的一个集合。我们只需在访问关系中实体之前调用其Load即可,当然提前判断该实体是否已经加载


是一种比较好的实践。如下代码所示:


 1 using (Entities entities = new Entities())
 2 {
 3   var query = (from o in entities.Orders
 4                where o.Customers.CustomerID == "ALFKI"
 5                select o);
 6   foreach (Orders order in query)
 7   {
 8     if (!order.CustomersReference.IsLoaded)
 9       order.CustomersReference.Load();
10     Console.WriteLine(order.OrderID + " --- " + 
11       order.Customers.CompanyName);
12   }
13 }
 


预先加载
先看代码示例


 1 using (Entities entities = new Entities())
 2 {
 3   var query = (from o in entities.Orders.Include("Customers")
 4                where o.ShipCountry == "USA"
 5                select o);
 6 
 7   foreach (Orders order in query)
 8     Console.WriteLine(order.OrderID + " --- " + 
 9       order.Customers.CompanyName);
10 }


查询中针对 Orders 实体调用的 Include 方法接受了一个参数,该参数在本示例中将要求查询不仅要检


索 Orders,而且还要检索相关的 Customers。这将生成单个 SQL 语句,它会加载满足 LINQ 查询条件


的所有 Order 和 Customer。 


两种加载关系实体的方式的选择根据:如果针对关系数据你只需做一到两次查询,则使用显示加载更高


效,如果要持续访问关系实体中数据,则使用预先加载。


关系下的添加,更新与删除与上述操作基本相同,唯一需要注意的是删除操作不支持级联删除,需要手


工遍历所有的相关项并将其一一删除。注意这里删除操作不能使用foreach来遍历需要删除的关系实体。


取而代之的有两种方法:


while法
1 while (result.Order_Details.Count > 0)
2 {
3   // 删除操作…
4 }
 


ToList法(以非联机方式操作)
1 var items = result.Order_Details.ToList();
2 foreach (var item in items)
3 {
4   // 删除操作…
5 }
 


最新补充
Entity Framework在开发中的应用 – Entity Framework与控件
.NET Framework提供了许多xxxDataSource控件,如SqlDataSource,ObjectDataSource等,这些数据源


控件大大方便了我们的数据绑定操作。不幸的是目前还没有针对Entity Framework的数据源控件发布,


但是将数据绑定到诸如ListBox,Grrdview或DetailsView控件也是很简单的。这源于使用ObjectContext


操作返回的IQueryable<T>对象或是使用EntityClient查询返回的ObjectQuery对象都实现了IEnumerable


接口。这样很容易将这些数据绑定到数据显示控件。更新操作可以按上文所述在相应的时间处理函数中


写更新EDM的程序即可。


Entity Framework的链接字符串
默认情况下(Visual Studio对Entity Framework数据项目的默认设置),EDM这个XML文件被作为资源在编


译时嵌入到程序集中。这种情况下当更改EDM后需要重新编译这个程序集才能使更改生效。通过更改项目


属性也可以让EDM作为三个独立的XML文件存在于项目中。为了让应用程序可以找到EDM(无论其以什么方


式存储)需要一个链接字符串来指示EDM所在的位置。实体模型设计器生成的链接字符串如下所示:


1 <add name="ASSEntities"
2 connectionString="
3 metadata=res://*/ass.csdl|
4 res://*/ass.ssdl|
5 res://*/ass.msl;
6 provider=System.Data.SqlClient;
7 provider connection string=&quot;Data Source=(local);Initial Catalog=ASS;Integrated 


Security=True;MultipleActiveResultSets=True&quot;" 
8 providerName="System.Data.EntityClient" />


http://msdn.microsoft.com/zh-cn/library/cc716756.aspx


关键的一点应用程序是怎样找到这个字符串的,对于使用EntityClient的情况,可以直接将连接字符串


赋给EntityConnection的ConnectionString属性,另外对于使用ObjectContext,其可以自动由配置文件


检索这个连接字符串。
========

Code First 一



public class Race : IEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}
public class Hero : IEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsSuperHero { get; set; }
    public virtual Race Race { get; set; }
}
  注意Hero类中的Race属性被定义成了virtual,这是为了延迟加载。但是不同于NH的是,不写


virtual不会报错,而是在使用时报出空引用异常。
这里插一句,在领域模型中必须向ORM妥协是让我非常不爽的地方,从使用NH的时候我就非常不喜欢


virtual这一点。NH lead Ayende Rahien 推荐了virtuosity,可以尝试一下。
  下一步,我们要创建自己的DbContext了
  按照官方blog的walk through上的写法,大概会写成:
public class EfDbContext : DbContext
{
    public DbSet<Hero> Heros { get; set; }
    public DbSet<Race> Races { get; set; }

  但是这样不就不能完成我们“任意类映射到数据库上”的需求了么?难道可续新加了一个类Abc,我


们就要加一行代码public DbSet<Abc> Abcs{get;set;}么?
  解决办法是使用DbContext类的Set方法(说实话从方法名实在看不出这个方法是取DbSet的),用法


类似如下:
public IEnumerable<TEntity> FindAll()
{
    return m_dbContext.Set<TEntity>();
}
  接下来就是读写数据了
  但是在这里让我们稍作停顿,思考一下我们的架构。应该让领域层(及更上层)使用基础结构层的


DbContext对象么?不。有如下几点原因:
上层应该对基础结构的实现一无所知,而DbContext是EF的对象,暴露了太多EF的细节。
DbContext是数据库访问的入口,提供了很多能力。这些能力是否应该向上层开放是很值得商榷的问题。


例如DbContext的生命周期由谁管理?如果上层可以直接使用DbContext那么意味着上层有能力new它


Dispose它,也即拥有了管理它生命周期的权利。
  基于上述原因,在领域层抽象出接口,让EF层来实现成了我的选择。在这个系列的第一章里,我们


只用到了查询,所以接口也只包含了查询功能。
public interface IRepository<TEntity>
    where TEntity : IEntity
{
    IEnumerable<TEntity> FindAll();


}
public class EntityRepository<TEntity> : IRepository<TEntity>
    where TEntity : class, IEntity
{
    private readonly DbContext m_dbContext;


    public EntityRepository(DbContext dbContext)
    {
        m_dbContext = dbContext;
    }


    public IEnumerable<TEntity> FindAll()
    {
        return m_dbContext.Set<TEntity>();
    }
}
  领域层和基础设施层的双向依赖是一个很古老的问题。可以用依赖注入框剪来解决。
  <object id="dbContext" type="EfSample.Model.Ef.EfDbContext ,EfSample.Model.Ef" 


scope="request">
    ...
  </object>
  <object id="repositoryBase" abstract="true" scope="request">
    <constructor-arg index="0" ref="dbContext"/>
  </object>
  <object id="heroRepository" 


type="EfSample.Model.Ef.Repositories.EntityRepository&lt;Hero&gt;, EfSample.Model.Ef" 


parent="repositoryBase"/>
  可以看到了依赖注入框架顺便替我们解决了DbContext的生命周期问题——per-request的管理。
  到这里,上层就可以使用IRepository接口来读取数据了。但是如果你真的去尝试,会发现立即抛异


常,为什么呢?因为EF的Code Firs模式有一套默认的convention来做类型到数据库的映射,而我上面给


出的数据库命名实在和默认convention差太远了。
  于是让我们来创建自己的Mapping
  让我们来回顾一下我们的Mapping规则:
Type->Table
有tbl前缀
每个单词之间用下划线分割
全部使用小写
Property->Column
以类名为前缀
每个单词之间用下划线分割
全部使用小写
  方案看似是很简单的:修改EF默认的conventions就好了嘛~~但是EF Code Firt不提供这个能力。。



EF Team说这个feature有一些易用性的问题,但是在RC前没时间搞所以就不提供了。。。这里有篇博文


讲如何打破这种限制,其实就是用反射将自定的convention强行写到Conventions集合里。本文还是不采


用这种方法了。
  只能在写自己的DbContext的时候override OnModelCreating方法。具体来讲有两种做法:
  一是在这个方法里写所有mapping信息:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{            modelBuilder.Entity<Hero>()                .ToTable("tbl_hero");
    modelBuilder.Entity<Hero>()                .Property(x => x.Id).HasColumnName


("hero_id");
     foreach (var mapping in Mappings)
    {
        mapping.RegistTo(modelBuilder.Configurations);
    }
}
  另一种是为每个类创建自己的EntityTypeConfiguration,把它插入到


modelBuilder.Configurations里。本例中采用后一种办法。一来后一种方法隔离了各种Type的Mapping


;二来后者对依赖注入也有比较好的支持。
  然而modelBuilder.Configurations的Add方法并不是一个很好的API,要求传入的泛型类型无法协变


,写不出这样的代码:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    foreach (var mapping in Mappings)
    {
        modelBuilder.Configurations.Add(mapping);
    }
}
  作为妥协方案,只能让各个mapping类把自己注册到modelBuilder.Configurations里。代码是这种


感觉:
public class EfDbContext : DbContext
{
    public IList<IMapping> Mappings { get; set; }
    
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        foreach (var mapping in Mappings)
        {
            mapping.RegistTo(modelBuilder.Configurations);
        }
    }
}
public class HeroMapping : EntityMappingBase<Hero>, IMapping
{
    public HeroMapping(IMappingStrategy<string> tableNameMappingStrategy) : base


(tableNameMappingStrategy)
    {
        Property(e => e.Id).HasColumnName(ColumnNameMappingStrategy.Value.To("Id"));
        Property(e => e.Name).HasColumnName(ColumnNameMappingStrategy.Value.To("Name"));
        Property(e => e.IsSuperHero).HasColumnName(ColumnNameMappingStrategy.Value.To


("IsSuperHero"));
        //Property(e => e.Race).HasColumnName


(AddUnderscoresBetweenWordsThenToLowerMappingStrategy.Value.To("Race"));
        HasRequired(h => h.Race).WithMany().Map(
            config => config.MapKey(ColumnNameMappingStrategy.Value.To("RaceId")));
    }


    #region Implementation of IMapping


    public void RegistTo(ConfigurationRegistrar configurationRegistrar)
    {
        configurationRegistrar.Add(this);
    }


    #endregion
}
  这样我们就可以注入EfDbContext的Mappings属性了:
<object id="dbContext" type="EfSample.Model.Ef.EfDbContext ,EfSample.Model.Ef" 


scope="request">
  <property name="Mappings">
    <list element-type="EfSample.Model.Ef.Mappings.IMapping ,EfSample.Model.Ef">
      <ref object="heroMapping"/>
      <ref object="raceMapping"/>
    </list>
  </property>
</object>
  另外,我也使用了依赖注入tableNameMappingStrategy的方式让mapping有更多的灵活性。不过


ColumnNameMappingStrategy就比较难依赖注入了,因为要依赖运行时的TypeName作为前缀。
  总结
  至此,我们已经能从数据库中把数据取出来了。回顾一下需求,我们大体实现了这样几点:
  1、查
  2、一对多
  3、可以映射到现有数据库上
  4、可以让任意类映射到数据库
  5、pre-request的DbContext生命周期管理。
========

Code First 二



  总之这一次的目标是:
实现一个完整的IRepository(添加增删改能力)
领域对象的继承
事物
  首先来看IRepository
  我的接口如下:
public interface IRepository<TEntity>
    where TEntity : IEntity
{
    IEnumerable<TEntity> FindAll();
    TEntity FindById(int id);
    void Add(TEntity entity);
    void Delete(TEntity entity);
    void Update(TEntity entity);
}
应该算是一个最基本的仓储接口了。
  其中前几个接口都是很好实现的,上次提及的DbSet对象提供了相应的接口,直接调用即可,代码是


类似这样的。
protected DbSet<TEntity> DbSet
{
    get { return m_dbContext.Set<TEntity>(); }
}


public IEnumerable<TEntity> FindAll()
{
    return DbSet;
}


public TEntity FindById(int id)
{
    return DbSet.SingleOrDefault(entity => entity.Id == id);
}


public void Add(TEntity entity)
{
    DbSet.Add(entity);
    m_dbContext.SaveChanges();
}


public void Delete(TEntity entity)
{
    DbSet.Remove(entity);
    m_dbContext.SaveChanges();
}
  关键问题是最后的Update方法
  DbSet对象并没有提供相应的接口,为什么呢?因为EF相信自己的Self Tracking能力。也就是说,


EF认为把一个entity从context中加载出来,做一些变更,然后直接SaveChanges就可以了,不需要特意


提供一个Update方法。
  但是这里有一个前提,就是“entity是从context中加载出来”。如果entity是新new出来的呢?比


如在MVC里,entity很可能是ModelBinder帮我们new出来的,context对它一无所知,直接SaveChanges显


然不会有任何效果。
  那么如何让context可以理解一个新new出来的entity呢?这要从EF处理entity状态开始说起。
  EF定义了如下几种State(注意这个枚举是Flag)
[Flags]
public enum EntityState
{
    Detached = 1,
    Unchanged = 2,
    Added = 4,
    Deleted = 8,
    Modified = 16,
}
  其中Detached状态,就是entity还没有attach到context(实际上是Attach到某个DbSet上)的状态


。具体怎么做呢?直接上代码:
public void Update(TEntity entity)
{
    var entry = m_dbContext.Entry(entity);
    if (entry.State == EntityState.Detached)
    {
        //DbSet.Attach(entity);
        entry.State = EntityState.Modified;
    }
    m_dbContext.SaveChanges();
}
  可以看到上面的代码给出了两种办法,一种是直接修改entry的State,另一种是调用DbSet对象的


Attach方法。
  注意到DbContext.Entry方法取出的DbEntityEntry对象。利用这个对象可以做很多有用的事哦~~园


子里的EF专家LingzhiSun有一篇blog,大家可以去读读。
  不过这个实现有一个缺陷
  我们上面谈到过,上面这个实现实际上是把entity attach到了对应的DbSet上。但是如果你的代码


是类似如下的,就可能产生问题(没有亲试,感觉上是这样的= =)
var heros = repository.FindAll();
var hero = heros.First(h => h.Id == 1);
var heroNew = new Hero
{
    Id = hero.Id,
    Name = hero.Name,
    Race = hero.Race
};
repository.Update(heroNew);
  应该是会抛出来一个异常说“An object with the same key already exists in the 


ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.



  异常说的很明白,你的DbSet已经加载过一次id为1的对象了,当试图去attach另一个id为1的对象的


时候EF就会无所适从。
  那是不是说刚才给出的那个实现根本就行不通呢?不是的!事实上微软官方的文章上就是采用这种


方法的。关键就在于当你尝试去attach一个entity的时候,要保证DbSet还没有加载过!我们看上面那篇


微软的文章里是如何保证这一点的:
public class BlogController : Controller
{
    BlogContext db = new BlogContext();
 
    //...
 
    [HttpPost]
    public ActionResult Edit(int id, Blog blog)
    {
        try
        {
          db.Entry(blog).State = EntityState.Modified;
          db.SaveChanges(); 
          return RedirectToAction("Index");
        }
        catch
        {
            return View();
        }
    }
}
很明显,在执行Edit这个Action之前,DbSet没有加载过,因为MVC帮我们保证了DbContext实例是request结


束就被销毁的。
  也就是说,结论是使用这种Update实现方式对context的生命周期是有要求的.当然我的例子中


context的生命周期也是per-request的所以没关系。
  那么如果我们想使用其他的context生命周期管理方式呢?比如希望整个application只有一个


context实例?
  让我们来给出另一种实现
  回过头来想一想在实现Update这个方法的时候我们最初遇到的问题:entity不是从context中加载的


而是直接new出来的。
  那么我们手动的来加载一次就好了么,代码类似于这样:
public void Update(Hero entity)
{
    var entry = m_dbContext.Entry(entity);
    if (entry.State == EntityState.Detached)
    {
        Hero entityToUpdate = FindById(entity.Id);
        entityToUpdate.Id = entity.Id;
        entityToUpdate.Name = entity.Name;
        entityToUpdate.Race = entity.Race;
    }
    m_dbContext.SaveChanges();
}
  不过由于失去了泛型的优势,给每个domain model都要实现一个Update方法比较烦,可以用一些框


架来解决这个问题,例如EmitMapper(园子里也讨论过这个东西)
public void Update(TEntity entity)
{
    var entry = m_dbContext.Entry(entity);
    if (entry.State == EntityState.Detached)
    {
        var entityToUpdate = FindById(entity.Id);
        EmitMapper.ObjectMapperManager.DefaultInstance.GetMapper<TEntity, TEntity>().Map


(entity, entityToUpdate);
    }
    m_dbContext.SaveChanges();
}
当然这个实现也有不好的地方例如说当domain里有一些跟ORM没关系的property时也会被EmitMapper改写


掉。
  下一个议题是领域对象的继承
  让领域对象实现继承的好处是不言而喻的,可以使用到多态等OO带来的好处。相对的就对ORM提出了


更高的要求。
  我们知道映射对象树到数据库有三种经典的实现方式:Table Per Type、Table Per Hierarchy和


Table Per Concrete class,这次我们来实践最简单的一种:Table Per Hierarchy。
  回想我们上一次的类:
public class Hero : IEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsSuperHero { get; set; }
    public virtual Race Race { get; set; }
}
  把它拆成两个有继承关系的类:
public class Hero : IEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    //public bool IsSuperHero { get; set; }
    public virtual Race Race { get; set; }
}
public class SuperHero : Hero
{
}
在EF Code First中这种单表继承的映射关系是这样来写的:
Map<Hero>(hero => hero.Requires(ColumnNameMappingStrategy.Value.To


("IsSuperHero")).HasValue(false)).ToTable(tableNameMappingStrategy.To("Hero"));
Map<SuperHero>(hero => hero.Requires(ColumnNameMappingStrategy.Value.To


("IsSuperHero")).HasValue(true)).ToTable(tableNameMappingStrategy.To("Hero"));
  另外两种方式的实现也不复杂,可以参考这里。这个实例还是CTP5的API,跟4.1最终版有些区别不


过应该影响不大。
  今天最后的议题是事物
  可以用TransactionScope来管理,虽然看起来有些浪费,毕竟例子中不涉及Transaction传播,连


DbContext都只有一个实例。代码如下:
[HttpPost]
public ActionResult Edit(TEntity entity)
{
    try
    {
        using (var scope = new TransactionScope())
        {
            ModelRepository.Update(entity);
            scope.Complete();
        }
        return RedirectToAction("Index");
    }
    catch
    {
        return View();
    }
}
========

链接

http://blog.csdn.net/csh624366188/article/details/7065036/
http://kb.cnblogs.com/zt/ef/

http://blog.csdn.net/bcbobo21cn/article/details/43966541


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值