实体框架-代码优先-无法存储List <String>


106

我写了这样的课:

class Test
{
    [Key]
    [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    [Required]
    public List<String> Strings { get; set; }

    public Test()
    {
        Strings = new List<string>
        {
            "test",
            "test2",
            "test3",
            "test4"
        };
    }
}

internal class DataContext : DbContext
{
    public DbSet<Test> Tests { get; set; }
}

运行后代码:

var db = new DataContext();
db.Tests.Add(new Test());
db.SaveChanges();

我的数据正在保存,但只是Id。我没有适用于“ 字符串”列表的任何表或关系。

我究竟做错了什么?我也尝试过制作Strings, virtual但是它并没有改变任何东西。

谢谢您的帮助。


3
您如何将List <sting>存储到数据库中?那行不通。将其更改为字符串。
Wiktor Zychla

4
如果有列表,它必须指向某个实体。为了让EF存储列表,它需要第二张表。在第二张表中,它将把列表中的所有内容放到一起,并使用外键指向您的Test实体。因此,创建一个具有Idproperty和MyStringproperty 的新实体,然后列出该实体。
Daniel Gabriel

1
是的...它不能直接存储在db中,但我希望Entity Framework创建新的实体来单独执行此操作。谢谢您的意见。
保罗

Answers:


161

实体框架不支持原始类型的集合。您可以创建一个实体(将保存到另一个表中),也可以进行一些字符串处理以将列表另存为字符串,并在实体实现后填充列表。


如果实体包含实体列表怎么办?如何保存映射?
A_Arnold

取决于-最有可能到一个单独的表。
Pawel

可以尝试序列化然后压缩并保存json格式的文本,或者在需要时进行加密并保存。无论哪种方式,您都无法让框架为您完成复杂的类型表映射。
尼古拉斯

89

EF Core 2.1+:

属性:

public string[] Strings { get; set; }

OnModelCreating:

modelBuilder.Entity<YourEntity>()
            .Property(e => e.Strings)
            .HasConversion(
                v => string.Join(',', v),
                v => v.Split(',', StringSplitOptions.RemoveEmptyEntries));

5
EF Core的绝佳解决方案。虽然似乎有问题,但将char转换为字符串。我必须像这样实现它:.HasConversion(v => string.Join(“;”,v),v => v.Split(new char [] {';'},StringSplitOptions.RemoveEmptyEntries));
彼得·科勒

8
这是恕我直言的唯一真正正确的答案。所有其他要求您更改模型,这违反了领域模型应忽略持久性的原则。(如果您使用单独的持久性和领域模型,那很好,但实际上很少有人这样做。)
Marcell Toth

2
您应该接受我的编辑请求,因为您不能使用char作为string.First的第一个参数,并且如果您还想提供StringSplitOptions,则必须提供char []作为string.Split的第一个参数。
多米尼克

2
在.NET Core中可以。我在一个项目中使用了这段确切的代码。
萨珊,

2
在.NET Standard中不可用
Sasan

54

该答案基于@Sasan@CAD bloke提供的答案

仅适用于EF Core 2.1+(不兼容.NET Standard)(Newtonsoft JsonConvert

builder.Entity<YourEntity>().Property(p => p.Strings)
    .HasConversion(
        v => JsonConvert.SerializeObject(v),
        v => JsonConvert.DeserializeObject<List<string>>(v));

使用EF Core流畅配置,我们List可以对JSON进行序列化/反序列化。

为什么此代码是您可以争取的一切的完美结合:

  • Sasn原始答案的问题是,如果列表中的字符串包含逗号(或选择作为分隔符的任何字符),它将变成一个大混乱,因为它将单个条目转换为多个条目,但这是最容易阅读和阅读的。最简洁。
  • CAD bloke答案的问题在于它丑陋且需要更改模型,这是一种不良的设计实践(请参阅Marcell Toth对Sasan答案的评论)。但这是数据安全的唯一答案。

7
太棒了,这应该公认的答案
Shirkan '19

1
我希望这可以在.NET Framework和EF 6中工作,这是一个非常优雅的解决方案。
CAD bloke

这是一个了不起的解决方案。谢谢
Marlon

您有能力查询该字段吗?我的尝试惨败:var result = await context.MyTable.Where(x => x.Strings.Contains("findme")).ToListAsync();找不到任何东西。
Nicola Iarocci

3
要回答我自己的问题,请引用文档:“使用值转换可能会影响EF Core将表达式转换为SQL的能力。在这种情况下将记录警告。正在考虑删除这些限制以用于将来的发行版。” -仍然会很好。
Nicola Iarocci

44

我知道这是一个古老的问题,Pawel提供了正确的答案,我只想显示一个代码示例,该示例演示如何进行一些字符串处理,并避免为原始类型的列表使用额外的类。

public class Test
{
    public Test()
    {
        _strings = new List<string>
        {
            "test",
            "test2",
            "test3",
            "test4"
        };
    }

    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    private List<String> _strings { get; set; }

    public List<string> Strings
    {
        get { return _strings; }
        set { _strings = value; }
    }

    [Required]
    public string StringsAsString
    {
        get { return String.Join(',', _strings); }
        set { _strings = value.Split(',').ToList(); }
    }
}

1
为什么不使用静态方法而不使用公共属性?(或者我是否表现出我的程序编程偏见?)
Duston

@randoms为什么必须定义2个列表?一个作为属性,一个作为实际列表?如果您还可以解释这里的绑定是如何工作的,我将不胜感激,因为该解决方案对我而言不是很好,并且我无法在此处弄清楚绑定。谢谢
LiranBo

2
有一个私有列表,其中有两个关联的公共属性:字符串(将在应用程序中用于添加和删除字符串),以及StringsAsString(将被保存到数据库的值,以逗号分隔的列表)。我不确定我要问的是什么,绑定是私有列表_strings,它将两个公共属性连接在一起。
随机

1
请记住,此答案不会,以字符串形式转义(逗号)。如果列表中的字符串包含一个或多个,(逗号),则该字符串将拆分为多个字符串。
Jogge

2
string.Join逗号中应使用双引号(用于字符串)而不是单引号(用于字符)。参见msdn.microsoft.com/en-us/library/57a79xd0(v=vs.110).aspx
Michael Brandon Morris

29

JSON.NET可以解救。

您将其序列化为JSON以保留在数据库中,然后反序列化以重新构成.NET集合。与Entity Framework 6和SQLite相比,这似乎比我预期的要好。我知道您要的,List<string>但这是一个更复杂的集合的示例,该集合可以正常工作。

我用标记了持久属性,[Obsolete]因此对我来说很明显,在正常的编码过程中“这不是您要查找的属性”。“ real”属性用标记,[NotMapped]因此实体框架将忽略它。

(不相关的切线):您可以对更复杂的类型执行相同的操作,但是您需要问问自己,是否只是让查询对象的属性对自己来说太难了?(是的,在我的情况下)。

using Newtonsoft.Json;
....
[NotMapped]
public Dictionary<string, string> MetaData { get; set; } = new Dictionary<string, string>();

/// <summary> <see cref="MetaData"/> for database persistence. </summary>
[Obsolete("Only for Persistence by EntityFramework")]
public string MetaDataJsonForDb
{
    get
    {
        return MetaData == null || !MetaData.Any()
                   ? null
                   : JsonConvert.SerializeObject(MetaData);
    }

    set
    {
        if (string.IsNullOrWhiteSpace(value))
           MetaData.Clear();
        else
           MetaData = JsonConvert.DeserializeObject<Dictionary<string, string>>(value);
    }
}

我发现此解决方案非常难看,但实际上是唯一的理智解决方案。如果在字符串中包含拆分字符,则所有提供使用任何字符加入列表然后将其拆分回列表的选项都可能变成一团糟。杰森应该更加理智。
Mathieu VIALES

1
我最终给出了一个答案这个答案这个答案和另一个答案的“合并”,以利用对方的优点来解决每个答案问题(丑陋/数据安全)。
Mathieu VIALES

13

只是为了简化-

实体框架不支持原语。您可以创建一个包装它的类,或者添加另一个属性以将列表格式化为字符串:

public ICollection<string> List { get; set; }
public string ListString
{
    get { return string.Join(",", List); }
    set { List = value.Split(',').ToList(); }
}

1
这是在列表项不能包含字符串的情况下。否则,您将需要对其进行转义。或者针对更复杂的情况序列化/反序列化列表。
亚当·塔尔

3
另外,不要忘记在ICollection属性上使用[NotMapped]
Ben Petersen

7

当然,帕维尔给出了正确的答案。但是我在这篇文章中发现,自EF 6+起,可以保存私有属性。所以我更喜欢这段代码,因为您不能以错误的方式保存字符串。

public class Test
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    [Column]
    [Required]
    private String StringsAsStrings { get; set; }

    public List<String> Strings
    {
        get { return StringsAsStrings.Split(',').ToList(); }
        set
        {
            StringsAsStrings = String.Join(",", value);
        }
    }
    public Test()
    {
        Strings = new List<string>
        {
            "test",
            "test2",
            "test3",
            "test4"
        };
    }
}

6
如果字符串包含逗号怎么办?
白垩

4
我不建议这样做。StringsAsStrings仅在更改Strings 引用时更新,并且示例中发生的唯一时间是分配。Strings分配后在列表中添加或删除项目不会更新StringsAsStrings后备变量。实现此目的的正确方法是将其公开显示StringsAsStringsStrings列表视图,而不是相反。getStringsAsStrings属性的访问器中将这些值连接在一起,然后在访问器中将它们拆分set
jduncanator

为了避免添加私有属性(并非没有副作用),请将序列化属性的设置器设为私有。jduncanator当然是正确的:如果您没有抓住列表操作(使用ObservableCollection?),那么EF不会注意到这些更改。
Leonidas

正如@jduncanator提到的那样,对列表进行修改(例如在MVVM中绑定)时,此解决方案不起作用
Ihab Hajj

7

稍微调整@Mathieu Viales答案,这是一个使用新的System.Text.Json序列化程序的.NET Standard兼容片段,从而消除了对Newtonsoft.Json的依赖。

using System.Text.Json;

builder.Entity<YourEntity>().Property(p => p.Strings)
    .HasConversion(
        v => JsonSerializer.Serialize(v, default),
        v => JsonSerializer.Deserialize<List<string>>(v, default));

请注意,虽然在这两个第二个参数Serialize(),并Deserialize()通常是可选的,你会得到一个错误:

表达式树可能不包含使用可选参数的调用或调用

分别将其显式设置为默认值(null)可以清除该问题。


3

您可以使用此ScalarCollection容器来限制数组并提供一些操作选项(Gist):

用法:

public class Person
{
    public int Id { get; set; }
    //will be stored in database as single string.
    public SaclarStringCollection Phones { get; set; } = new ScalarStringCollection();
}

码:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;

namespace System.Collections.Specialized
{
#if NET462
  [ComplexType]
#endif
  public abstract class ScalarCollectionBase<T> :
#if NET462
    Collection<T>,
#else
    ObservableCollection<T>
#endif
  {
    public virtual string Separator { get; } = "\n";
    public virtual string ReplacementChar { get; } = " ";
    public ScalarCollectionBase(params T[] values)
    {
      if (values != null)
        foreach (var item in Items)
          Items.Add(item);
    }

#if NET462
    [Browsable(false)]
#endif
    [EditorBrowsable(EditorBrowsableState.Never)]
    [Obsolete("Not to be used directly by user, use Items property instead.")]
    public string Data
    {
      get
      {
        var data = Items.Select(item => Serialize(item)
          .Replace(Separator, ReplacementChar.ToString()));
        return string.Join(Separator, data.Where(s => s?.Length > 0));
      }
      set
      {
        Items.Clear();
        if (string.IsNullOrWhiteSpace(value))
          return;

        foreach (var item in value
            .Split(new[] { Separator }, 
              StringSplitOptions.RemoveEmptyEntries).Select(item => Deserialize(item)))
          Items.Add(item);
      }
    }

    public void AddRange(params T[] items)
    {
      if (items != null)
        foreach (var item in items)
          Add(item);
    }

    protected abstract string Serialize(T item);
    protected abstract T Deserialize(string item);
  }

  public class ScalarStringCollection : ScalarCollectionBase<string>
  {
    protected override string Deserialize(string item) => item;
    protected override string Serialize(string item) => item;
  }

  public class ScalarCollection<T> : ScalarCollectionBase<T>
    where T : IConvertible
  {
    protected override T Deserialize(string item) =>
      (T)Convert.ChangeType(item, typeof(T));
    protected override string Serialize(T item) => Convert.ToString(item);
  }
}

8
看起来有点工程?
Falco Alexander

1
@FalcoAlexander我已经更新了我的帖子...也许有些冗长,但是可以完成工作。确保替换NET462为适当的环境或将其添加到其中。
Shimmy Weitzhandler,2017年

1
+1,以将其组合在一起。解决方案对于存储字符串数组有点过分:)
GETah
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.