如何在Entity Framework中维护有序列表?

时间:2013-02-25 02:43:17

标签: entity-framework .net-4.0

更改简单列表中元素的顺序,不会粘在实体框架中。原因很简单,因为订购信息永远不会存储在数据库中。

是否有人遇到过与实体框架一起使用的有序列表的通用实现?

要求是允许用户重新排序所选项目的列表,并且需要保留项目的顺序。

4 个答案:

答案 0 :(得分:5)

概述

虽然实现这一点似乎没有任何“魔力”,但我们已经使用了一种模式来解决这个问题,特别是在处理对象的层次结构时。它归结为三个关键因素:

  1. 构建与您的域模型分开的实体模型。这样可以提供良好的关注点分离,有效地允许您的域模型的设计和更改,而不会因持久性细节而陷入困境。
  2. 使用 AutoMapper 来克服实体和域模型之间的映射麻烦。
  3. 实施自定义值解析程序以在两个方向上映射列表。
  4. 魔术

    由于模型通常包含对象之间的分层和循环引用,因此可以使用以下Map<>()方法在自定义映射期间避免StackOverflow错误

    private class ResolveToDomain : IValueResolver
    {
        ResolutionResult Resolve(ResolutionResult rr)
        {
            //...
            ((MappingEngine) rr.Context.Engine).Map<Product, ProductEntity>(rr.Context, subProduct)
            //...
        }
    }
    

    守则

    域名模型。请注意,子产品列表顺序很重要。

    class Product
    {
        public Product ParentProduct { get; set; }
        public string Name { get; set; }
        public IList<Product> Subproducts { get; set; } 
    }
    

    实体模型

    class ProductEntity
    {
        public int Id { get; set; }
        public ProductEntity ParentProduct { get; set; }
        public string Name { get; set; }
        public IList<ProductSubproductEntity> Subproducts { get; set; } 
    }
    
    class ProductSubproductEntity
    {
        public int ProductId { get; set; }
        public ProductEntity Product { get; set; }
        public int Order { get; set; }
        public ProductEntity Subproduct { get; set; }
    }
    

    实体框架背景

    class Context : DbContext
    {
        public DbSet<ProductEntity> Products { get; set; }
    
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<ProductEntity>()
                .HasOptional(e => e.ParentProduct);
    
            modelBuilder.Entity<ProductSubproductEntity>()
                .HasKey(e => new {e.ProductId, e.Order})
                .HasRequired(e => e.Product)
                .WithMany(e => e.Subproducts)
                .HasForeignKey(e => e.ProductId);
    
            base.OnModelCreating(modelBuilder);
        }
    }
    

    AutoMapper配置

    class Mappings : Profile
    {
        protected override void Configure()
        {
            Mapper.CreateMap<Product, ProductEntity>()
                .ForMember(m => m.Subproducts, a => a.ResolveUsing<ProductSubproductResolver>());
    
            Mapper.CreateMap<ProductEntity, Product>()
                .ForMember(m => m.Subproducts, a => a.ResolveUsing<ProductSubproductEntityResolver>());
    
            base.Configure();
        }
    }
    
    class ProductSubproductResolver : IValueResolver
    {
        public ResolutionResult Resolve(ResolutionResult rr)
        {
            var result = new List<ProductSubproductEntity>();
            var subproductsSource = ((Product) rr.Context.SourceValue).Subproducts;
    
            if (subproductsSource == null) return rr.New(null);
    
            for (int i = 0; i < subproductsSource.Count; i++)
            {
                var subProduct = subproductsSource[i];
    
                result.Add(new ProductSubproductEntity()
                {
                    Product = (ProductEntity)rr.Context.DestinationValue,
                    Order = i,
                    Subproduct = ((MappingEngine) rr.Context.Engine).Map<Product, ProductEntity>(rr.Context, subProduct)
                });
            }
            return rr.New(result);
        }
    }
    
    class ProductSubproductEntityResolver: IValueResolver
    {
        public ResolutionResult Resolve(ResolutionResult rr)
        {
            var subproductEntitiesSource = ((ProductEntity) rr.Context.SourceValue).Subproducts;
    
            if (subproductEntitiesSource == null) return rr.New(null);
    
            var result = subproductEntitiesSource.OrderBy(p => p.Order).Select(p => 
                ((MappingEngine) rr.Context.Engine).Map<ProductEntity, Product>(rr.Context, p.Subproduct))
                .ToList();
            return rr.New(result);
        }
    }
    

    <强>用法

    private static IList<Product> CreateDomainProducts()
    {
        var result = new List<Product>();
    
        var mainProduct1 = new Product()
        {
            Name = "T-Shirt"
        };
        var subProduct1 = new Product()
        {
            ParentProduct = mainProduct1,
            Name = "T-Shirt (Medium)",
        };
        var subProduct2 = new Product()
        {
            ParentProduct = mainProduct1,
            Name = "T-Shirt (Large)",
        };
        mainProduct1.Subproducts = new []
        {
            subProduct1,
            subProduct2
        };
    
        var mainProduct2 = new Product()
        {
            Name = "Shorts"
        };
    
        result.Add(mainProduct1);
        result.Add(mainProduct2);
    
    
        return result;
    }
    
    static void Main(string[] args)
    {
        Mapper.Initialize(a => a.AddProfile<Mappings>());
        Database.SetInitializer(new DropCreateDatabaseAlways<Context>());
    
        var products = CreateDomainProducts();
        var productEntities = Mapper.Map<IList<ProductEntity>>(products);
    
        using (var ctx = new Context())
        {
            ctx.Products.AddRange(productEntities);
            ctx.SaveChanges();
        }
        // Simulating a disconnected scenario...
        using (var ctx = new Context())
        {
            var productEntity = ctx.Products
                .Include(p => p.Subproducts)
                .Include(p => p.Subproducts.Select(p2 => p2.Subproduct))
                .OrderBy(p=>p.Name)
                .ToList();
    
            var productsResult = Mapper.Map<IList<Product>>(productEntity);
    
            // Should be 'T-Shirt (Medium)'
            Console.WriteLine(productsResult[1].Subproducts[0].Name);
            // Should be 'T-Shirt (Large)'
            Console.WriteLine(productsResult[1].Subproducts[1].Name);
        }
    }
    

    瞧。希望有所帮助!

答案 1 :(得分:3)

这里没有魔力。如果要在列表中保留特定的项目顺序(除了可重复的顺序,例如名称),您必须在数据库中存储序列号。

答案 2 :(得分:2)

对于重新排序数据库,不会有这样的实现。默认情况下,数据库中的数据按聚集索引进行物理排序,聚类索引实质上是由主键排序。

你为什么要这样做? EF鼓励所有订购都通过LINQ查询完成。

如果您希望优化查找,可以通过修改为迁移生成的代码在数据库上创建其他非聚集索引:

CreateTable(
                "dbo.People",
                c => new
                    {
                        ID = c.Int(nullable: false, identity: true), 
                        Name = c.String()
                    })
                .PrimaryKey(t => t.ID)
                .Index(t => t.Name); // Create an index

请注意,这不会影响数据库中的物理排序,但会加快查找速度,但这需要通过较慢的写入/更新来平衡。

答案 3 :(得分:0)

通过下面的链接,我在一篇文章中找到了解决这一难题的方法:

User-defined Order in SQL

本文分析了在更改列表顺序期间生成顺序索引值的不同方法。我发现本文中提到的算法在最小限制下表现出色。此算法称为 True Fractions (真分数),它会生成如下图所示的订单索引:

enter image description here

我已经准备了一个代码示例,可以通过EF Core和InMemory数据库实现此方法。

.NET Fiddle Code Sample