【问题标题】:ASP.NET MVC 4, EF5, Unique property in model - best practice?ASP.NET MVC 4,EF5,模型中的唯一属性 - 最佳实践?
【发布时间】:2013-05-16 17:29:55
【问题描述】:

ASP.NET MVC 4、EF5、代码优先、SQL Server 2012 Express

在模型中强制执行唯一值的最佳做法是什么?我有一个地方类,它有一个“url”属性,每个地方都应该是唯一的。

public class Place
{
      [ScaffoldColumn(false)]
      public virtual int PlaceID { get; set; }

      [DisplayName("Date Added")]
      public virtual DateTime DateAdded { get; set; }

      [Required(ErrorMessage = "Place Name is required")]
      [StringLength(100)]
      public virtual string Name { get; set; }

      public virtual string URL { get; set; }
};

为什么不能在其上放置一个 [唯一] 数据注释?

我已经看到 1 或 2 次关于此的讨论,但没有谈到最佳实践。使用 Code First,您能以某种方式告诉数据库在数据库中的字段上设置唯一约束吗?

什么是最简单的方法 - 什么是最佳实践?

【问题讨论】:

  • 我现在使用(并推荐)Dapper.net 和数据注释的价值 - 喜欢它。
  • 我不再首先使用代码 - 这似乎是一个已经死掉的想法。

标签: c# asp.net-mvc entity-framework asp.net-mvc-4 entity-framework-5


【解决方案1】:

尽管听起来很疯狂,但如今的最佳做法是使用内置验证,而是使用 FluentValidation。然后代码将非常易于阅读和超级可维护,因为验证将在单独的类上进行管理,这意味着更少的意大利面条代码。

您要达到的目标的伪示例。

[Validator(typeof(PlaceValidator))]
class Place
{
    public int Id { get; set; }
    public DateTime DateAdded { get; set; }
    public string Name { get; set; }
    public string Url { get; set; }
}

public class PlaceValidator : AbstractValidator<Place>
{
    public PlaceValidator()
    {
        RuleFor(x => x.Name).NotEmpty().WithMessage("Place Name is required").Length(0, 100);
        RuleFor(x => x.Url).Must(BeUniqueUrl).WithMessage("Url already exists");
    }

    private bool BeUniqueUrl(string url)
    {
        return new DataContext().Places.FirstOrDefault(x => x.Url == url) == null
    }
}

【讨论】:

  • 有趣。所以我只需要将它用于任何唯一属性 - 其他所有内容都可以保持原样,传统的 Code First 对吗?这是否涵盖所有基础、竞争条件、数据库上 100% 可靠的唯一性等?
  • 我还需要安装fluentvalidation nuget包吗?
  • 这个例子可以毫无问题地使用 POCO,我在我所有的项目中都使用它。如您所见,它将检查TryValidateModel() 上的数据库中的唯一性,之后您通常会调用_db.SaveChanges();,因此基本上没有任何延迟。是的,你当然需要 nuget 包。
  • 谢谢。我注意到您已将“名称”添加为不为空。你能不能只在类定义中放一个 [Required] 数据注释?或者这会取代所有数据注释/完全阻止它们工作 - 无论是对于这个模型还是任何模型?
  • 谁的最佳实践?也有人可能会争辩说,这是为了获得最小收益而需要承担的大量代码
【解决方案2】:

此链接可能会有所帮助: https://github.com/fatihBulbul/UniqueAttribute

[Table("TestModels")]
public class TestModel
{

    [Key]
    public int Id { get; set; }

    [Display(Name = "Some", Description = "desc")]
    [Unique(ErrorMessage = "This already exist !!")]
    public string SomeThing { get; set; }
}

【讨论】:

    【解决方案3】:

    唯一的方法是在生成迁移后更新迁移(假设您正在使用它们),以便对列实施唯一约束。

    public override void Up() {
      // create table
      CreateTable("dbo.MyTable", ...;
      Sql("ALTER TABLE MyTable ADD CONSTRAINT U_MyUniqueColumn UNIQUE(MyUniqueColumn)");
    }
    public override void Down() {
      Sql("ALTER TABLE MyTable DROP CONSTRAINT U_MyUniqueColumn");
    }
    

    不过,最难的一点是在访问数据库之前在代码级别强制执行约束。为此,您可能需要一个包含唯一值完整列表的存储库,并确保新实体不会通过工厂方法违反它。

    // Repository for illustration only
    public class Repo {
      SortedList<string, Entity1> uniqueKey1 = ...; // assuming a unique string column 
      public Entity1 NewEntity1(string keyValue) {
        if (uniqueKey1.ContainsKey(keyValue) throw new ArgumentException ... ;
        return new Entity1 { MyUniqueKeyValue = keyValue };
      }
    }
    

    参考资料:

    脚注:

    有很多要求 [Unique] 在代码中优先,但看起来它甚至没有制作版本 6:http://entityframework.codeplex.com/wikipage?title=Roadmap

    您可以尝试在这里投票:http://data.uservoice.com/forums/72025-entity-framework-feature-suggestions/suggestions/1050579-unique-constraint-i-e-candidate-key-support

    【讨论】:

    • 我从未使用过存储库或工厂 - 任何链接/教程推荐?我想我也可以“手动”验证新地点的用户输入以确保 URL 是唯一的?如果验证存在缺陷,是否可以在对象级别强制执行 - 比如在 SQL Server 中设置唯一约束(也许你的建议涵盖了这一点?)似乎有更好的解决方案,因为这是一个常见的要求?
    • 注意 - 这是一个规模相对较小的应用程序,目前只有一个开发人员 - 所以一个简单/快速的解决方案会很好。我还需要为用户强制执行唯一字段 - 例如在他们的 Twitter 句柄或电子邮件中。
    • @user2254951。使用提供的代码和链接进行编辑。你需要一个公共的地方来检查唯一性,因此我的单例建议因为它会在 IIS 中的 appdomain 的持续时间内存在于内存中,你只需要在第一次查询时播种它,但这很容易。
    • @user2254951。小型应用程序可能会一直等到您写入数据库,但创建工厂方法相对容易,并且可以节省额外的代码,以便在您保存时从重复和异常中恢复。
    • 谢谢 - 会看的。我还发现了在 Seed 方法中直接向数据库添加 UNIQUE 约束的建议 - 首先破坏代码的纯度,但应该有效?!
    【解决方案4】:

    您可以在将数据保存到数据库表之前在代码级别进行此检查。

    您可以尝试在您的视图模型上使用 Remote 数据注释来进行异步验证,以使 UI 响应更快。

    public class CreatePlaceVM
    {
      [Required]
      public string PlaceName { set;get;}
    
      [Required]
      [Remote("IsExist", "Place", ErrorMessage = "URL exist!")
      public virtual string URL { get; set; }
    }
    

    确保您的Placecontroller 中有一个IsExists 操作方法,它接受URL 参数并再次检查您的表格并返回true 或false。

    这个msdn link 有一个示例程序来展示如何实现远程属性来进行即时验证。

    另外,如果您使用的是存储过程(出于某种原因),您可以在 INSERT 查询之前进行 EXISTS 检查。

    【讨论】:

    • 这看起来很酷 - 我是否必须实现其他任何东西才能使其工作?如果另一个用户同时执行相同的查询怎么办 - 有什么方法可以防止更深/数据库级别的重复?
    • 执行两次(一次使用上述方式(指示用户更改重复值),然后在保存之前的 HttpPost 操作方法中执行第二次(检查数据库以查看是否有该项目存在与否)。除非你有一些巨大的性能问题(我猜你没有),否则你不会有任何问题
    • 是的,这样的检查应该在与插入新 URL 的代码的事务中,以确保没有竞争条件。打开事务,是否存在?,插入,提交
    【解决方案5】:

    我解决了在您的验证流程中启用构造函数注入的一般问题,集成到正常的 DataAnnotations 机制中,而无需借助 this answer 中的框架,从而可以编写:

    class MyModel 
    {
        ...
        [Required, StringLength(42)]
        [ValidatorService(typeof(MyDiDependentValidator), ErrorMessage = "It's simply unacceptable")]
        public string MyProperty { get; set; }
        ....
    }
    
    public class MyDiDependentValidator : Validator<MyModel>
    {
        readonly IUnitOfWork _iLoveWrappingStuff;
    
        public MyDiDependentValidator(IUnitOfWork iLoveWrappingStuff)
        {
            _iLoveWrappingStuff = iLoveWrappingStuff;
        }
    
        protected override bool IsValid(MyModel instance, object value)
        {
            var attempted = (string)value;
            return _iLoveWrappingStuff.SaysCanHazCheez(instance, attempted);
        }
    }
    

    使用一些辅助类(看那里),你可以把它连接起来,例如在 ASP.NET MVC 中就像在 Global.asax 中一样:-

    DataAnnotationsModelValidatorProvider.RegisterAdapterFactory(
        typeof(ValidatorServiceAttribute),
        (metadata, context, attribute) =>
            new DataAnnotationsModelValidatorEx(metadata, context, attribute, true));
    

    【讨论】:

      【解决方案6】:

      在我的 ASP.NET Razor 页面项目中遇到了类似的问题。创建自定义 UniqueDataAttribute 不起作用,因为在编辑时,如果您不更改唯一字段,它将引发错误。

      我需要唯一的书名。我就是这样解决的:

      1. 我通过 EF Core 迁移向数据库中的字段添加了唯一约束。在 ApplicationDbContext 类中添加以下内容,然后运行迁移。

      代码:

      protected override void OnModelCreating(ModelBuilder builder)
              {
                  builder.Entity<Book>()
                      .HasIndex(u => u.Name)
                      .IsUnique();
              }
      
      1. 接下来,创建了如下的辅助/扩展方法。

      代码:

              // Validate uniqueness of Name field in database.
              // If validation is done on existing record, pass the id of the record.
              // Else, if validating new record Name, then id is set to dummy key integer -1
              public static bool UniqueNameInDb(this string data, ApplicationDbContext db, int id = -1)
              {
                  var duplicateData = from o in db.Book
                                      where o.Name == data && o.Id != id
                                      select o;
                  if(duplicateData.Any())
                  {
                      return false;
                  }
                  return true;
              }
          }
      
      1. 然后在 OnPost() 方法的 Create and Edit page model 中使用如下。

      创建模型:

      public async Task<IActionResult> OnPost()
              {
                  if(ModelState.IsValid)
                  {
                      if (!Book.Name.UniqueNameInDb(_db)) //<--Uniqueness validation
                      {
                          ModelState.AddModelError("Book.Name", "Name already exist"); //<-- Add error to the ModelState, that would be displayed in view.
                          return Page();
                      }
      
                      await _db.Book.AddAsync(Book);
                      await _db.SaveChangesAsync();
      
                      return RedirectToPage("Index");
      
                  }
                  else
                  {
                      return Page();
                  }
              }
      

      编辑模型:

      public async Task<IActionResult> OnPost()
              {
                  if(ModelState.IsValid)
                  {
                      var bookFromDb = await _db.Book.FindAsync(Book.Id);
                      if (!Book.Name.UniqueNameInDb(_db, Book.Id)) //<--Uniqueness validation
                      {
                          ModelState.AddModelError("Book.Name", "Name already exist"); //<-- Add error to the ModelState, that would be displayed in view.
                          return Page();
                      }
                      bookFromDb.Name = Book.Name;
                      bookFromDb.Author = Book.Author;
      
                      await _db.SaveChangesAsync();
      
                      return RedirectToPage("Index");
                  }
      
                  return Page();
              }
      

      PS:您的 Razor 视图应该在表单中设置模型验证以捕获并显示错误。

      <div class="text-danger" asp-validation-summary="ModelOnly"></div>
      

      及以下针对该字段的验证。

      <span asp-validation-for="Book.Name" class="text-danger"></span>
      

      【讨论】:

        【解决方案7】:

        好吧,这很简单,但是如果这有效与否,请不客气。只需在添加新用户之前检查电子邮件是否已存在即可。

        if (!db.Users.Any(x => x.Email == data.Email))
         // your code for adding
        else
         // send a viewbag to the view
         //  ViewBag.error = "Email Already Exist";
        

        【讨论】:

          猜你喜欢
          • 2011-09-16
          • 1970-01-01
          • 1970-01-01
          • 2011-04-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-02-27
          相关资源
          最近更新 更多