5

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

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

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

4

4 回答 4

8

概述

尽管似乎没有任何“魔法”来实现这一点,但我们已经使用了一种模式来解决这个问题,尤其是在处理对象层次结构时。它归结为三个关键点:

  1. 构建一个与您的域模型分开的实体模型。这样做的好处是提供了良好的关注点分离,有效地允许设计和更改您的域模型,而不会被持久性细节所困扰。
  2. 使用AutoMapper克服实体模型和域模型之间映射的麻烦。
  3. 实现自定义值解析器以双向映射列表。

魔法

因为模型中经常包含对象之间的分层和循环引用,所以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);
    }
}

瞧。希望有帮助!

于 2014-11-29T21:31:40.917 回答
4

这里没有魔法。如果您想在列表中保留特定的项目顺序(而不是按名称的可复制顺序),您必须在数据库中存储一个序列号。

于 2013-02-25T16:20:07.923 回答
2

不会有用于在数据库上重新排序的实现。数据库中的数据在物理上默认是按照聚集索引排序的,本质上是按照主键排序的。

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

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

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

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

于 2013-02-25T02:59:04.177 回答
1

为了找到解决这个挑战的方法,我通过以下链接看到了一篇文章:

SQL中的用户自定义顺序

本文分析了在改变列表顺序时产生顺序索引值的不同方法。我发现本文中提到的算法在最小限制下非常高效。该算法称为True Fractions,它生成的订单索引如下图所示:

在此处输入图像描述

我准备了一个代码示例,我通过 EF Core 和 InMemory 数据库实现了这种方法。

.NET Fiddle 代码示例

于 2020-08-18T21:10:00.097 回答