Entity Framework - Код по-перше - не вдається зберегти список <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 .

Що я роблю неправильно? Я також намагався зробити Strings, virtual але це нічого не змінило.

Дякую за твою допомогу.


3
Як ви очікуєте, що список <sting> зберігається у db? Це не вийде. Змініть його на рядок.
Wiktor Zychla

4
Якщо у вас є список, він повинен вказувати на якусь сутність. Для зберігання списку EF потрібна друга таблиця. У другій таблиці він помістить усе зі свого списку, а за допомогою зовнішнього ключа вкаже на вашу Testсутність. Тож складіть нову сутність із Idмайном та MyStringвласністю, а потім складіть її список.
Даніель Габріель

1
Правильно ... Його не можна зберігати безпосередньо в db, але я сподівався, що Entity Framework створить нову сутність для цього самостійно. Дякую за ваші коментарі.
Пол

Відповіді:


161

Entity Framework не підтримує колекції примітивних типів. Ви можете або створити сутність (яка буде збережена в іншій таблиці), або здійснити обробку рядків, щоб зберегти список як рядок та заповнити список після матеріалізації.


що робити, якщо суб'єкт господарювання містить Перелік організацій? як буде збережено картографування?
A_Arnold

Залежить - швидше за все, до окремої таблиці.
Павло

можна спробувати серіалізувати, а потім стиснути та зберегти текст у форматі 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. Хоча, схоже, виникає проблема з перетворенням рядків у рядки. Мені довелося реалізувати його так: .HasConversion (v => string.Join (";", v), v => v.Split (новий char [] {';'}, StringSplitOptions.RemoveEmptyEntries));
Пітер Коллер

8
Це єдина справді правильна відповідь ІМХО. Усі інші вимагають, щоб ви змінили свою модель, і це порушує принцип, згідно з яким моделі домену повинні бути неусвідомленими. (Добре, якщо ви використовуєте окремі моделі стійкості та домену, але мало хто насправді це робить.)
Marcell Toth,

2
Ви повинні прийняти мій запит на редагування, оскільки ви не можете використовувати char як перший аргумент string.Join і вам потрібно надати char [] як перший аргумент string.Split, якщо ви також хочете надати StringSplitOptions.
Домінік

2
У .NET Core ви можете. Я використовую саме цей фрагмент коду в одному зі своїх проектів.
Сасан

2
Недоступно у .NET Standard
Сасан

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.

Чому цей код є ідеальним поєднанням всього, до чого можна прагнути:

  • Проблема з оригінальною відповіддю Сасна полягає в тому, що він перетвориться на великий безлад, якщо рядки зі списку містять коми (або будь-який символ, обраний як роздільник), оскільки він перетворить один запис у кілька записів, але його найпростіше читати і найбільш стисло.
  • Проблема у відповіді Блока CAD полягає в тому, що вона некрасива і вимагає змінити модель, що є поганою практикою дизайну (див. Коментар Марселла Тота щодо відповіді Сасана ). Але це єдина відповідь, яка безпечна для даних.

7
браво, це, мабуть, має бути прийнята відповідь
Ширкан

1
Я хочу, щоб це працювало в .NET Framework & EF 6, це дійсно елегантне рішення.
CAD

Це дивовижне рішення. Дякую
Марлон

Чи можете ви запитувати на цьому полі? Мої спроби нещасно зазнали невдачі: var result = await context.MyTable.Where(x => x.Strings.Contains("findme")).ToListAsync();нічого не знаходить.
Нікола Яроччі

3
Щоб відповісти на моє власне запитання, цитуючи документи : "Використання перетворень значень може вплинути на здатність EF Core перекладати вирази в SQL. У таких випадках буде записано попередження. Видалення цих обмежень розглядається для майбутнього випуску." - Хоча б все-таки добре.
Нікола Яроччі

44

Я знаю, що це давнє запитання, і Павел дав правильну відповідь , я просто хотів показати приклад коду, як зробити деяку обробку рядків, і уникнути зайвого класу для списку примітивного типу.

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
Чому б не статичні методи замість використання загальнодоступних властивостей? (Або я показую свої упередження щодо процесуального програмування?)
Дастон

@randoms Чому потрібно визначити 2 списки? одна як власність і одна як власне список? Буду вдячний, якщо ви також можете пояснити, як працює прив'язка тут, оскільки це рішення не працює для мене добре, і я не можу з’ясувати прив'язку тут. Спасибі
LiranBo

2
є один приватний список, у якому пов'язані дві загальнодоступні властивості, Strings, які ви будете використовувати у вашому додатку для додавання та видалення рядків, і StringsAsString - значення, яке буде збережено у db, як список, розділений комами. Я не дуже впевнений, про що ви запитуєте, однак, обов'язковим є приватний список _strings, який з'єднує дві публічні властивості разом.
randoms

1
Будь ласка, майте на увазі, що ця відповідь не уникає ,(кома) у рядках. Якщо рядок у списку містить одну чи більше ,(кома), рядок розбивається на кілька рядків.
Джогге

2
У string.Joinкомах повинні бути оточені подвійні лапки (для рядка), а не одиничні лапки (для знака). Дивіться msdn.microsoft.com/en-us/library/57a79xd0(v=vs.110).aspx
Майкл Брендон Морріс

29

JSON.NET на допомогу.

Ви серіалізуєте його в JSON, щоб зберегти базу даних, і десеріалізувати його для відновлення колекції .NET. Це, здається, працює краще, ніж я очікував, що це стосується Entity Framework 6 & SQLite. Я знаю, що ви просили, List<string>але ось приклад ще більш складної колекції, яка працює чудово.

Я позначив збережене властивість, [Obsolete]щоб мені було дуже очевидно, що "це не та властивість, яку ви шукаєте" у звичайному процесі кодування. "Справжнє" властивість позначено тегом, [NotMapped]тому Entity Framework ігнорує його.

(Незв'язана дотична): Ви можете зробити те ж саме з більш складними типами, але вам потрібно запитати себе, чи просто ви зробили запит щодо властивостей цього об'єкта занадто важким для себе? (так, у моєму випадку).

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);
    }
}

Я вважаю це рішення досить потворним, але насправді це єдине розумне. Усі параметри, що пропонують приєднатись до списку, використовуючи будь-який символ, а потім розділити його назад, можуть перетворитись у дикий безлад, якщо символ розбиття включений до рядків. Json повинен бути набагато більш здоровим.
Mathieu VIALES

1
Я в кінцевому підсумку склав відповідь, що є "злиттям" цієї та іншої, щоб виправити кожну проблему відповіді (потворність / безпека даних), використовуючи сильні моменти іншої.
Mathieu VIALES

13

Просто для спрощення -

Entity Framework не підтримує примітивів. Ви або створіть клас, щоб обернути його, або додаєте інше властивість для форматування списку у вигляді рядка:

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

1
Це в тому випадку, якщо елемент списку не може містити рядок. Інакше вам потрібно буде уникнути цього. Або серіалізувати / дезаріалізувати список для складніших ситуацій.
Адам Тал

3
Також не забудьте використати [NotMapped] у власності ICollection
Бен Петерсен

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резервну змінну. Правильним способом реалізації цього було б викриття StringsAsStringsяк перегляд Stringsсписку, а не навпаки. Об’єднайте значення разом у getдоступному StringsAsStringsресурсі та розділіть їх на доступ set.
jduncanator

Щоб уникнути додавання приватних властивостей (які не є побічними ефектами), зробіть сетер серіалізованої власності приватним. jduncanator, звичайно, має рацію: якщо ви не ловите маніпуляції зі списком (використовуєте ObservableCollection?), зміни не помітять EF.
Леонідас

Як зазначав @jduncanator, це рішення не працює, коли вноситься модифікація до списку (наприклад, прив'язка в MVVM)
Іхаб Хадж

7

Злегка щипаючи @Mathieu Viales «S відповідь , ось сумісний сниппет .NET Standard з використанням нового System.Text.Json серіалайзер , таким чином усуваючи залежність від 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
виглядає трохи над інженерно ?!
Фалько Олександр

1
@FalcoAlexander Я оновив свою посаду ... Можливо, трохи докладно, але робить роботу. Переконайтесь, що ви замінили NET462відповідним середовищем або додали до нього.
Шиммі Вайцхандлер

1
+1 за зусилля зі з’єднання цього. Рішення - це небагато зайвого для зберігання масиву рядків :)
GETah,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.