前言

和大家脱离了一段时间,有时候总想着时间挤挤总是会有的,但是并非人愿,后面会借助周末的时间来打理博客,如有问题可以在周末私信我或者加我QQ皆可,欢迎和大家一起探讨,本节我们来讨论EF Core中的一些问题后面陆陆续续会将EF Core中需要注意的地方补充上来,有些是我一直以来比较疏忽的地方,不喜勿喷。用在实际项目中的时候才发现和平时所学有很大差异,靠着项目才能检验出真理。

EntityFramework Core问题集锦

更新单个实体

更新单个实体的方式有两种:

(1)查询出实体进行赋值更新

说的更专业一点则是已被跟踪的实体进行赋值更新,此时实体已被快照,此时进行更新时只需要调用SaveChanges或者SaveChangesAsync,当已赋值属性与快照中值不同时,此时调用SaveChangesAsync或者SaveChanges方法时会将此属性的状态即(IsModified)修改为True,否则为False。代码大概如下:

        public async Task<bool> UpdateStatus(int id, byte status)
{
var blog = _efCoreContext.Blogs.Find(id);
blog.Status = status;
var effectRows = await _efCoreContext.SaveChangesAsync(CancellationToken.None);
if (effectRows > )
{
return true;
}
return false;
}

但是如上又带来一个问题,我们通过影响行数来获取是否更新成功,如果想更新某一列,但是此列的值未进行改变,此时与快照中的值一致,则受影响行数为0,结果返回的更新失败,如下所示:

在这种情况就需要一个扩展方法来显式指定更新属性即使值未发生改变也将其属性状态IsModified修改为True,这样才不会导致值未改变但是更新失败的情况,即如下:

 _efCoreContext.Entry(blog).Property(d => d.Status).IsModified = true;

(2)未查询出实体进行赋值更新。

当此实体未进行查询,此时需要调用Update来将此实体状态中所有属性的IsModified修改为True,此时代码大概如下:

        public async Task<bool> UpdateStatus(Blog blog)
{
_efCoreContext.Blogs.Update(blog);
var effectRows = await _efCoreContext.SaveChangesAsync(CancellationToken.None);
if (effectRows > )
{
return true;
}
return false;
}

也就是说如果是明确实体所有属性都会更改则可以利用Update方法来更新所有属性,否则不需要更新的属性比如常见场景:数据库中表中数据创建时间下次进行更新时是不需要更新,如若调用Update方法,如果对创建时间赋值会进行覆盖,未赋值则会显示DateTime默认时间。

批量更新之表达式树

批量更新的场景大有,在我们项目中选择多个产品将产品的状态更新为下架状态,下面我们来还原场景。创建批量更新接口,此时数据库中数据如下:

Task<bool> UpdateStatus(int[] ids);

我们将Blog中状态中为0的行更新为1,此时接口则如下:

        public async Task<bool> UpdateStatus(int[] ids)
{
var blogs = _efCoreContext.Blogs.Where(d => ids.Contains(d.Id)); blogs.Select(b => new Blog() { Id = b.Id, Status = }).ToList(); if (await _efCoreContext.SaveChangesAsync(CancellationToken.None) > )
{
return true;
}
return false;
}

此时更新肯定不能正确更新,其原因不必多讲,由于是更新集合中的指定属性,此时我写了关于单个和集合更新指定属性的扩展方法,如下:

    public static class EfCoreUpdateExe
{
public static void Update<T>(this EFCoreContext context, T entity, params Expression<Func<T, object>>[] properties) where T : class, new()
{
var dbEntityEntry = context.Entry(entity);
if (properties.Any())
{
foreach (var property in properties)
{
dbEntityEntry.Property(property).IsModified = true;
}
}
else
{
foreach (var rawProperty in dbEntityEntry.Entity.GetType().GetTypeInfo().DeclaredProperties)
{
var originalValue = dbEntityEntry.Property(rawProperty.Name).OriginalValue;
var currentValue = dbEntityEntry.Property(rawProperty.Name).CurrentValue;
foreach (var property in properties)
{
if (originalValue != null && !originalValue.Equals(currentValue))
dbEntityEntry.Property(property).IsModified = true;
} }
}
} public static void UpdateRange<TEntity>(this EFCoreContext context, IEnumerable<TEntity> entities, bool isNoTracking = true, params Expression<Func<TEntity, object>>[] properties) where TEntity : class, new()
{
foreach (var entity in entities)
{
var dbEntityEntry = context.Entry(entity);
//Notice that:当更新实体指定属性时,若实体从数据库中查询而出,此时实体已被跟踪,则无需处理,若实例化对象而更新对象指定属性,此时需要将其状态修改为Unchanged即需要附加
if (!isNoTracking) { dbEntityEntry.State = EntityState.Unchanged; }
if (properties.Any())
{
foreach (var property in properties)
{
dbEntityEntry.Property(property).IsModified = true;
}
}
else
{
foreach (var rawProperty in dbEntityEntry.Entity.GetType().GetTypeInfo().DeclaredProperties)
{
var originalValue = dbEntityEntry.Property(rawProperty.Name).OriginalValue;
var currentValue = dbEntityEntry.Property(rawProperty.Name).CurrentValue;
foreach (var property in properties)
{
if (originalValue != null && !originalValue.Equals(currentValue))
dbEntityEntry.Property(property).IsModified = true;
} }
}
}
}
}

然后代码更新代码修改如下:

        public async Task<bool> UpdateStatus(int[] ids)
{
var blogs = _efCoreContext.Blogs
.Where(d => ids.Contains(d.Id)); var updateProductList = blogs.Select(b => new Blog() { Id = b.Id, Status = }).ToList(); _efCoreContext.UpdateRange(updateProductList, true, d => d.Status); if (await _efCoreContext.SaveChangesAsync(CancellationToken.None) > )
{
return true;
}
return false;
}

此时与数据库会进行两次连接,一次是查询,一次是更新指定属性字段,通过SQL跟踪我们能看到如下语句:

SELECT [d].[Id]
FROM [Blog] AS [d]
WHERE [d].[Id] IN (, , , )
exec sp_executesql N'SET NOCOUNT ON;
UPDATE [Blog] SET [Status] = @p0
WHERE [Id] = @p1;
SELECT @@ROWCOUNT; UPDATE [Blog] SET [Status] = @p2
WHERE [Id] = @p3;
SELECT @@ROWCOUNT; UPDATE [Blog] SET [Status] = @p4
WHERE [Id] = @p5;
SELECT @@ROWCOUNT; UPDATE [Blog] SET [Status] = @p6
WHERE [Id] = @p7;
SELECT @@ROWCOUNT; ',N'@p1 int,@p0 tinyint,@p3 int,@p2 tinyint,@p5 int,@p4 tinyint,@p7 int,@p6 tinyint',@p1=2,@p0=1,@p3=3,@p2=1,@p5=5,@p4=1,@p7=6,@p6=1

最终正确更新如下:

除了上述通过写反射扩展方法来更新外属性外,一直在想着其中会进行两次数据库链接,进行一次数据库链接比较耗时,这个时候想到的只能执行SQL命令了。

批量更新之SQL命令

利用WHERE ....IN来进行更新,此时SQL更新代码则如下:

        public async Task<bool> UpdateStatus(int[] ids)
{
var testIds = string.Join(",", ids); var effctRow = await _efCoreContext.Database.ExecuteSqlCommandAsync("update dbo.Blog set [Status] = 1 where id in ({0})", CancellationToken.None, testIds);
if (effctRow > )
{
return true;
}
return false;
}

不知道各位看客发现什么没有,上述的代码是有问题的,哪里有问题,不知道的请看如下动态演示。

正常情况下WHERE...IN(2,3,5,6)而非上述“2,3,5,6”,此时我将上述代码修改为如下:

        public async Task<bool> UpdateStatus(int[] ids)
{
var blogIds = string.Empty;
foreach (var id in ids)
{
blogIds += $"{id},";
}
blogIds = blogIds.TrimEnd(',');
var effectRows = await _efCoreContext.Database.ExecuteSqlCommandAsync("update dbo.Blog set [Status] = 1 where id in ({0})", CancellationToken.None, blogIds);
if (effectRows > )
{
return true;
}
return false;
}

此时再来看看演示效果:

此时则报NVARCHAR转换到INT失败,那么粗暴一点将id转换为NVARCHAR:

 var effectRows = await _efCoreContext.Database.ExecuteSqlCommandAsync("update dbo.Blog set [Status] = 1 where cast(id as nvarchar(max)) in ({0})", CancellationToken.None, blogIds);

当然如上涉及到索引,通过函数转换不会走索引,我们正常情况下应该是定义一个变量将id进行转换,然后利用变量来进行包含。此时再来看演示效果:

此时压根都没去更新,我也是醉了,最后我们再来看一种情况,我们写SQL命令通过拼接的形式来进行,如下:

 var effectRows = await _efCoreContext.Database.ExecuteSqlCommandAsync($"update dbo.Blog set [Status] = 1 where id in ({blogIds})", CancellationToken.None);

此时居然更新成功了,其实我们利用上述字符串拼接的方式进行如下两种转换都会更新成功:

             //转换方式一
//var blogIds = string.Join(",", ids); //转换方式二
var blogIds = string.Empty;
foreach (var id in ids)
{
blogIds += $"{id},";
}
blogIds = blogIds.TrimEnd(',');
var effectRows = await _efCoreContext.Database.ExecuteSqlCommandAsync($"update dbo.Blog set [Status] = 1 where id in ({blogIds})", CancellationToken.None);

但是利用$符号本质无非是简化了string.format的书写罢了,容易导致SQL注入的问题,但是利用参数化SQL对于WHERE....IN情况就是无法进行更新,对于删除亦是如此,上述未曾演示利用SqlParameter来进行更新,如果你这样做了,结果依然一样不好使:

            var blogIds = string.Empty;
foreach (var id in ids)
{
blogIds += $"{id},";
}
blogIds = blogIds.TrimEnd(',');
var parameters = new SqlParameter[]
{
new SqlParameter("@ids",System.Data.SqlDbType.NVarChar,){ Value = blogIds }
};
var effectRows = await _efCoreContext.Database.ExecuteSqlCommandAsync("update dbo.Blog set [Status] = 1 where id in (@ids)", CancellationToken.None, parameters);

上述是对于更新的主键为INT的情况,若是主键为字符串,此时这种情况更加突出,因为对于字符串形式需要这样的格式IN('A','B','C'),此时我们将上述id看作为字符串,我们进行如下转换:

var blogIds = "'" + string.Join("','", ids) + "'";

然后去进行更新,参数正确,格式也正确,但是就是无法进行更新。最终统一得出的结论是:

进行批量更新或者删除的情况利用WHERE....IN参数化SQL无法进行更新或者删除,利用$或者string.format进行拼接却好使,但是会导致SQL注入。

上述演示EF Core版本为1.1.2,遇到这样的问题是在进行批量删除时,有人反问了批量删除不是有RemoveRange么,但是其中涉及到多表查询然后进行批量删除,就是期望达到一步到位的效果,最终没有办法,我采用LINQ的方法利用两步来进行批量删除,看到此文的你对于EF Core中利用SQL(WHERE....IN)命令来进行批量删除或者更新的情况见解是怎样,是否有遇到这样的问题,如果利用参数化SQL解决了问题的话望告知。

2017-08-07利用WHERE...IN参数化SQL批量更新或删除

这两天人感冒,什么都不想干,回来太早又没事干,于是乎再次回顾了下这个问题,我天真的以为在ADO.NET中利用WHERE...IN用SQL的方式来进行批量更新呢或者删除是好使的,结果一试居然一样不好使,有人想了为何不利用存储过程解决何必纠结于此,想了想就一句话的事情,没必要还搞个存储过程而且还要打开数据库操作(我懒)。最终还是利用原生的方式来解决这个问题,在WHERE...IN中将IN中的所有需要更新或者需要删除的数据生成参数的方式来解决即可,请往下看。

        public async Task<bool> UpdateStatus(string idsStr)
{
var ids = idsStr.Split(','); var parms = ids.Select((s, i) => "@p" + i.ToString()).ToArray(); var inclause = string.Join(",", parms); var parameters = new SqlParameter[parms.Length]; for (int i = ; i < ids.Length; i++)
{
parameters[i] = new SqlParameter()
{
Value = ids[i],
ParameterName = parms[i],
SqlDbType = SqlDbType.VarChar,
Size =
};
} var effectRows = await _efCoreContext.Database.ExecuteSqlCommandAsync($"UPDATE dbo.Blog SET [Status] = 1 WHERE Id in({inclause})", CancellationToken.None, parameters);
if (effectRows > )
{
return true;
}
return false;
}

接下来进行调用更新:

        [HttpGet("[action]")]
public async Task<IActionResult> Index()
{
var ids = "2,4,6,7";
var result = await _blogRepository.UpdateStatus(ids); return Ok();
}

数据库原始数据如下:

至此成功进行更新,上述代码则无需一一进行解释,简单易懂。为了方便调用,对于利用WHERE...IN利用进行批量更新或删除将其进行如下封装。

第一步:构造WHERE...IN中的参数

        public static string BuildWhereInClause<T>(string partialClause, string paramPrefix, IEnumerable<T> parameters)
{
string[] parameterNames = parameters.Select(
(paramText, paramNumber) => "@" + paramPrefix + paramNumber.ToString())
.ToArray(); string inClause = string.Join(",", parameterNames);
string whereInClause = string.Format(partialClause.Trim(), inClause); return whereInClause;
}

第二步:构造参数化Parameter

        public static SqlParameter[] Parameter<T>(string paramPrefix, IEnumerable<T> parameters)
{
string[] parameterValues = parameters.Select((paramText) => paramText.ToString()).ToArray(); string[] parameterNames = parameterValues.Select(
(paramText, paramNumber) => "@" + paramPrefix + paramNumber.ToString()
).ToArray();
var param = new SqlParameter[parameterNames.Length];
for (int i = ; i < parameterNames.Length; i++)
{
param[i] = new SqlParameter()
{
Value = parameterValues[i],
ParameterName = parameterNames[i],
SqlDbType = SqlDbType.VarChar
};
} return param;
}

最终定义一个静态类来调用如上两个方法:

    public static class SqlWhereInParameterBuilder
{
public static string BuildWhereInClause<T>(string partialClause, string paramPrefix, IEnumerable<T> parameters)
{
string[] parameterNames = parameters.Select(
(paramText, paramNumber) => "@" + paramPrefix + paramNumber.ToString())
.ToArray(); string inClause = string.Join(",", parameterNames);
string whereInClause = string.Format(partialClause.Trim(), inClause); return whereInClause;
} public static SqlParameter[] Parameter<T>(string paramPrefix, IEnumerable<T> parameters)
{
string[] parameterValues = parameters.Select((paramText) => paramText.ToString()).ToArray(); string[] parameterNames = parameterValues.Select(
(paramText, paramNumber) => "@" + paramPrefix + paramNumber.ToString()
).ToArray();
var param = new SqlParameter[parameterNames.Length];
for (int i = ; i < parameterNames.Length; i++)
{
param[i] = new SqlParameter()
{
Value = parameterValues[i],
ParameterName = parameterNames[i],
SqlDbType = SqlDbType.VarChar
};
} return param;
}
}

此时上述调用则进行如下调用:

            var sql = SqlWhereInParameterBuilder.BuildWhereInClause("UPDATE dbo.Blog SET [Status] = 1 WHERE Id in({0})", "Id", ids);
var parameters = SqlWhereInParameterBuilder.Parameter("id", ids);
var effectRows = await _efCoreContext.Database.ExecuteSqlCommandAsync(sql, CancellationToken.None, parameters);

一切从简,想要批量删除或者更新一步到位,你get到没有!遗留一个问题,上述只是针对单表而言,如果是多表,还有其他判断条件的参数,那么上述方法则不再适用,那又该如何改造呢?容我想想!

彩蛋

EntityFramework Core Shadow Property(狭隘属性)

在EF Core系列中介绍过EF Core中几个新特性比如可选键作为除主键外的唯一约束,BackFileds,关于BackFieds未曾用到也差不多忘记了,本节我们介绍一下EF Core漏掉的狭隘属性。

狭隘属性不是实体类的一部分,所以不存在于实体类中但是存在于实体模型中,那么到底该如何使用狭隘属性呢?使用狭隘属性主要在以下两个场景。

(1)当不想对实体类作出更改,但是需要添加一些字段到实体模型中。

(2)明确知道该属性是上下文中的一部分,但是不希望暴露这些属性。

例如在Blog实体类中存在如何字段和导航属性。

    public class Blog : IEntityBase
{
public int Id { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public byte Status { get; set; }
public IEnumerable<Post> Posts { get; set; }
}

常见场景:现在我们需要添加一个属性创建时间作为狭隘属性,此创建时间只有在实体添加状态时才有其值,其他状态值不发生改变且无需对外暴露,此时我们在映射中进行配置保持实体类洁净,如下:

        public override void Map(EntityTypeBuilder<Blog> b)
{
b.ToTable("Blog"); b.HasKey(k => k.Id); b.Property(p => p.Url);
b.Property(p => p.Name);
b.Property(p => p.Status).HasColumnType("TINYINT").IsRequired(); b.Property<DateTime>("CreatedTime");
}

那么如何对CreatedTime进行设置值和获取值呢?Change Tracker API负责维护狭隘属性,当我们创建Blog时为其狭隘属性赋值,如下:

        public async Task<bool> Create()
{
var blog = new Blog() { Name = "Jeffcky", Status = , Url = "http://www.cnblogs.com/CreateMyself" }; _efCoreContext.Entry(blog).Property("CreatedTime").CurrentValue = DateTime.Now; if (await _efCoreContext.SaveChangesAsync(CancellationToken.None) > )
{
return true;
}
return false;
}

由于对于大部分情况下都有其创建时间这一列,我们放在SaveChanges方法中并将其重写,如下:

        public override int SaveChanges()
{
var modifiedEntries = ChangeTracker
.Entries().Where(x => x.State == EntityState.Added); foreach (var item in modifiedEntries)
{
item.Property("CreatedTime").CurrentValue = DateTime.Now;
}
return base.SaveChanges();
} public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
var modifiedEntries = ChangeTracker
.Entries().Where(x => x.State == EntityState.Added); foreach (var item in modifiedEntries)
{
item.Property("CreatedTime").CurrentValue = DateTime.Now;
}
return await base.SaveChangesAsync();
}

此时创建Blog则改写为如下:

        public async Task<bool> Create()
{
var blog = new Blog() { Name = "Jeffcky", Status = , Url = "http://www.cnblogs.com/CreateMyself" }; _efCoreContext.Add(blog); if (await _efCoreContext.SaveChangesAsync(CancellationToken.None) > )
{
return true;
}
return false;
}

那么问题来了,如果配置的狭隘属性在实体类中已存在那么是否会抛出异常呢?不会,自动将已存在的实体类中同名的名称配置成狭隘属性。当然我们也可以通过如下来起别名:

b.Property<DateTime>("CreatedTime").HasColumnName("CreatedDate");

结论:狭隘属性应是对已存在的实体类添加但是不会去修改狭隘属性值。

那么最后一个问题又来了,在LINQ中如何引用狭隘属性进行查询呢?如下:通过EF.Property<>实现引用狭隘属性:

var cList = _efCoreContext.Blogs
.OrderBy(b => EF.Property<DateTime>(b, "CreatedTime")).ToList();

总结

有一段时间没写博客感觉有点生硬,后面会陆陆续续捡起来并将项目中遇到的问题进行总结,如有疑问或言论不对之处,请指教。see u.

EntityFramework Core查询问题集锦(一)的更多相关文章

  1. EntityFramework Core问题处理集锦(一)

    前言 和大家脱离了一段时间,有时候总想着时间挤挤总是会有的,但是并非人愿,后面会借助周末的时间来打理博客,如有问题可以在周末私信我或者加我QQ皆可,欢迎和大家一起探讨,本节我们来讨论EF Core中的 ...

  2. EntityFramework Core 2.0执行原始查询如何防止SQL注入?

    前言 接下来一段时间我们来讲讲EntityFramework Core基础,精简的内容,深入浅出,希望为想学习EntityFramework Core的童鞋提供一点帮助. EntityFramewor ...

  3. EntityFramework Core 2.0 Explicitly Compiled Query(显式编译查询)

    前言 EntityFramework Core 2.0引入了显式编译查询,在查询数据时预先编译好LINQ查询便于在请求数据时能够立即响应.显式编译查询提供了高可用场景,通过使用显式编译的查询可以提高查 ...

  4. EntityFramework Core笔记:查询数据(3)

    1. 基本查询 1.1 加载全部数据 using System.Linq; using (var context = new LibingContext()) { var roles = contex ...

  5. Cookies 初识 Dotnetspider EF 6.x、EF Core实现dynamic动态查询和EF Core注入多个上下文实例池你知道有什么问题? EntityFramework Core 运行dotnet ef命令迁移背后本质是什么?(EF Core迁移原理)

    Cookies   1.创建HttpCookies Cookie=new HttpCookies("CookieName");2.添加内容Cookie.Values.Add(&qu ...

  6. EntityFramework Core 3多次Include导致查询性能低之解决方案

    前言 上述我们简单讲解了几个小问题,这节我们再来看看如标题EF Core中多次Include导致出现性能的问题,废话少说,直接开门见山. EntityFramework Core 3多次Include ...

  7. EntityFramework Core 3.x添加查询提示(NOLOCK)

    前言 前几天看到有园友写了一篇关于添加NOLOCK查询提示的博文<https://www.cnblogs.com/weihanli/p/12623934.html>,这里呢,我将介绍另外一 ...

  8. EntityFramework Core Raw SQL

    前言 本节我们来讲讲EF Core中的原始查询,目前在项目中对于简单的查询直接通过EF就可以解决,但是涉及到多表查询时为了一步到位就采用了原始查询的方式进行.下面我们一起来看看. EntityFram ...

  9. EntityFramework Core 1.1 Add、Attach、Update、Remove方法如何高效使用详解

    前言 我比较喜欢安静,大概和我喜欢研究和琢磨技术原因相关吧,刚好到了元旦节,这几天可以好好学习下EF Core,同时在项目当中用到EF Core,借此机会给予比较深入的理解,这里我们只讲解和EF 6. ...

随机推荐

  1. java wait 和notify

    这几天自己学习了一下线程的知识,wait 方法使当前的线程等待,notify 方法 唤醒当前的线程的方法 th 线程在5的时候进行wait,此时主线程继续执行, 主线程执行到9的时候 唤醒 th 线程 ...

  2. java数据类型转换那点事

    public class kkk { /** * 先看看eclipse对于数值型转换会有哪些报错,但是有一点必须明确,eclipse不报错的,不一定就是说这种思维逻辑是对的 * 可以直接将代码复制过去 ...

  3. jrebel配置热部署参数

    jrebel配置热部署参数: -noverify -agentpath:D:/jrebel/lib/jrebel64.dll -Drebel.dirs=E:/workspace/item/src/ma ...

  4. JAVA基础——类和对象

    java类与对象学习笔记 一.成员变量和局部变量 (1)定义 1.成员变量 在类中定义,用来描述对象将要有什么.(默认的成员变量值为0) 2.局部变量 在类的方法中定义,在方法中临时保存数据. 演示示 ...

  5. Texlive + TexStudio + Language Tool Win7配置

    Texlive的配置很简单,安装的时候跟着向导一步一步安装就可以了. TexStudio也是同样的安装过程,没什么技巧.这里提一下界面颜色的配置.习惯了暗底白字,所以就google了一下相关的配置,大 ...

  6. 再谈AbstractQueuedSynchronizer:共享模式与基于Condition的等待/通知机制实现

    共享模式acquire实现流程 上文我们讲解了AbstractQueuedSynchronizer独占模式的acquire实现流程,本文趁热打铁继续看一下AbstractQueuedSynchroni ...

  7. meta小结

    mate 标签定义及使用说明 元数据(Metadata)是数据的数据信息. 标签提供了 HTML 文档的元数据.元数据不会显示在客户端,当时会被浏览器解析. META元素通常用于指定网页的描述,关键词 ...

  8. 关于Latex中插入Visio图片文字不显示的问题

    经过探索,将Visio保存为pdf格式是最完美的解决方式,因为pdf文件保存了所有格式和字体信息. Visio输出pdf时要使其符合PDF/A标准.如果包含Visio的多余信息,就会在一些低版本Lat ...

  9. Linux配置LNMP环境(二)配置PHP

    前言:本教程安装的PHP版本php-5.6.30(官方最后更新日期2017-01-19),教程编写日期2017-07-02.本教程中的下载地址是在写教程的时候从官方复制的,时间过长可能会有变化. 安装 ...

  10. python编程快速上手之第8章实践项目参考答案

    第8章实践项目之疯狂填词 创建一个一个疯狂填词(Mad Libs),程序,它将读入文本文件,并让用户在该文本文件中出现 ADJECTIVE,NOUN,VERB等单词的地方,加上他们自己的文本. 首先准 ...