Entity Framework - Code First - Can't Store List <String>

106

Napisałem taką klasę:

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

i

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

Po uruchomieniu kodu:

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

moje dane są zapisywane, ale tylko plik Id. Nie mam żadnych tabel ani relacji odnoszących się do listy ciągów .

Co ja robię źle? Próbowałem też stworzyć Strings, virtual ale to niczego nie zmieniło.

Dziękuję za pomoc.

Paweł
źródło
3
Jak oczekujesz, że List <sting> jest przechowywany w bazie danych? To nie zadziała. Zmień to na ciąg.
Wiktor Zychla
4
Jeśli masz listę, musi ona wskazywać na jakąś jednostkę. Aby EF mógł przechowywać listę, potrzebuje drugiej tabeli. W drugiej tabeli umieści wszystko z twojej listy i użyje klucza obcego, aby wskazać z powrotem twoją Testencję. Stwórz więc nową jednostkę z Idwłaściwościami i MyStringwłaściwościami, a następnie zrób ich listę.
Daniel Gabriel,
1
Dobrze ... Nie można go przechowywać bezpośrednio w bazie danych, ale miałem nadzieję, że Entity Framework utworzy nową jednostkę, która zrobi to samodzielnie. Dziękuję za twoje komentarze.
Paul,

Odpowiedzi:

161

Entity Framework nie obsługuje kolekcji typów pierwotnych. Możesz utworzyć encję (która zostanie zapisana w innej tabeli) lub wykonać przetwarzanie na łańcuchach, aby zapisać listę jako ciąg i zapełnić listę po zmaterializowaniu encji.

Paweł
źródło
co się stanie, jeśli jednostka zawiera listę podmiotów? jak zostanie zapisane mapowanie?
A_Arnold
Zależy - najprawdopodobniej do osobnej tabeli.
Paweł
może spróbować serializować, a następnie skompresować i zapisać sformatowany tekst json lub zaszyfrować i zapisać go w razie potrzeby. tak czy inaczej, nie możesz mieć frameworka wykonującego za Ciebie złożone mapowanie tabeli typów.
Niklas
90

EF Core 2.1+:

Własność:

public string[] Strings { get; set; }

OnModelCreating:

modelBuilder.Entity<YourEntity>()
            .Property(e => e.Strings)
            .HasConversion(
                v => string.Join(',', v),
                v => v.Split(',', StringSplitOptions.RemoveEmptyEntries));
Sasan
źródło
5
Świetne rozwiązanie dla EF Core. Chociaż wydaje się, że ma problem z konwersją znaków na ciąg. Musiałem to zaimplementować w taki sposób: .HasConversion (v => string.Join (";", v), v => v.Split (new char [] {';'}, StringSplitOptions.RemoveEmptyEntries));
Peter Koller,
8
To jedyna naprawdę poprawna odpowiedź IMHO. Wszystkie inne wymagają zmiany modelu, a to narusza zasadę, że modele domen powinny być ignorantami wytrwałości. (Jest dobrze, jeśli używasz oddzielnych modeli trwałości i domeny, ale niewiele osób to robi.)
Marcell Toth
2
Powinieneś zaakceptować moje żądanie edycji, ponieważ nie możesz użyć char jako pierwszego argumentu string.Join i musisz podać char [] jako pierwszy argument string.Split, jeśli chcesz również podać StringSplitOptions.
Dominik
2
W .NET Core możesz. Używam dokładnie tego fragmentu kodu w jednym z moich projektów.
Sasan
2
Niedostępne w .NET Standard
Sasan
54

Ta odpowiedź jest oparta na tych dostarczonych przez @Sasan i bloke @CAD .

Działa tylko z EF Core 2.1+ (niezgodnym z .NET Standard) (Newtonsoft JsonConvert)

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

Korzystając z konfiguracji EF Core Fluent, serializujemy / deserializujemy Listdo / z formatu JSON.

Dlaczego ten kod jest idealnym połączeniem wszystkiego, do czego możesz dążyć:

  • Problem z oryginalną odpowiedzią Sasn polega na tym, że zmieni się w duży bałagan, jeśli ciągi na liście będą zawierały przecinki (lub dowolny znak wybrany jako separator), ponieważ zamieni pojedynczy wpis w wiele wpisów, ale jest najłatwiejszy do odczytania i najbardziej zwięzłe.
  • Problem z odpowiedzią gościa z CAD polega na tym, że jest brzydka i wymaga zmiany modelu, co jest złą praktyką projektową (patrz komentarz Marcella Totha na temat odpowiedzi Sasana ). Ale to jedyna odpowiedź, która jest bezpieczna dla danych.
Mathieu VIALES
źródło
7
brawo, to prawdopodobnie powinna być zaakceptowana odpowiedź
Shirkan
1
Chciałbym, żeby to działało w .NET Framework i EF 6, to naprawdę eleganckie rozwiązanie.
Facet z CAD
To niesamowite rozwiązanie. Dziękuję
Marlon
Czy jesteś w stanie zapytać o to pole? Moje próby nie powiodły się: var result = await context.MyTable.Where(x => x.Strings.Contains("findme")).ToListAsync();nic nie znajduję.
Nicola Iarocci
3
Aby odpowiedzieć na moje własne pytanie, cytując dokumenty : „Użycie konwersji wartości może mieć wpływ na zdolność EF Core do tłumaczenia wyrażeń na język SQL. W takich przypadkach zostanie zarejestrowane ostrzeżenie. Rozważane jest usunięcie tych ograniczeń w przyszłej wersji”. - Mimo wszystko byłoby miło.
Nicola Iarocci
44

Wiem, że to stare pytanie, a Paweł udzielił poprawnej odpowiedzi. Chciałem tylko pokazać przykład kodu pokazujący, jak przetwarzać ciągi znaków i uniknąć dodatkowej klasy dla listy typów pierwotnych.

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(); }
    }
}
randoms
źródło
1
Dlaczego nie metody statyczne zamiast używania właściwości publicznych? (A może pokazuję swoje błędy związane z programowaniem proceduralnym?)
Duston
@randoms dlaczego konieczne jest zdefiniowanie 2 list? jeden jako własność, a drugi jako aktualna lista? Byłbym wdzięczny, gdybyś mógł również wyjaśnić, jak działa tutaj wiązanie, ponieważ to rozwiązanie nie działa dla mnie dobrze, a tutaj nie mogę rozgryźć wiązania. Dzięki
LiranBo
2
jest jedna lista prywatna, która ma dwie powiązane właściwości publiczne, Strings, których będziesz używać w swojej aplikacji do dodawania i usuwania ciągów oraz StringsAsString, która jest wartością, która zostanie zapisana w bazie danych, jako lista oddzielona przecinkami. Nie jestem jednak pewien, o co pytasz, powiązaniem jest prywatna lista _strings, która łączy ze sobą dwie właściwości publiczne.
randoms
1
Należy pamiętać, że ta odpowiedź nie jest zapisywana ,(przecinek) w łańcuchach. Jeśli ciąg na liście zawiera jeden lub więcej ,(przecinek), ciąg jest dzielony na wiele ciągów.
Jogge
2
W string.Joinprzecinek powinien być otoczony przez cudzysłów (na sznurku), a nie apostrofów (dla char). Zobacz msdn.microsoft.com/en-us/library/57a79xd0(v=vs.110).aspx
Michael Brandon Morris
29

JSON.NET na ratunek.

Możesz serializować go do formatu JSON, aby zachować go w bazie danych, i deserializować go w celu ponownego utworzenia kolekcji .NET. Wydaje się, że działa lepiej, niż się spodziewałem w przypadku Entity Framework 6 i SQLite. Wiem, o co prosiłeśList<string> ale oto przykład jeszcze bardziej złożonej kolekcji, która działa dobrze.

Oznaczyłem utrwaloną właściwość, [Obsolete]więc byłoby dla mnie bardzo oczywiste, że „to nie jest właściwość, której szukasz” w normalnym toku kodowania. Właściwość „real” jest oznaczona tagiem, [NotMapped]więc Entity framework ją ignoruje.

(niepowiązana styczna): Możesz zrobić to samo z bardziej złożonymi typami, ale musisz zadać sobie pytanie, czy sprawiłeś, że zapytanie o właściwości tego obiektu było dla siebie zbyt trudne? (tak, w moim przypadku).

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);
    }
}
Facet z CAD
źródło
Uważam to rozwiązanie za dość brzydkie, ale w rzeczywistości jest to jedyne rozsądne. Wszystkie opcje oferujące dołączenie do listy przy użyciu dowolnego znaku, a następnie podzielenie jej z powrotem, mogą przekształcić się w dziki bałagan, jeśli znak podziału jest zawarty w łańcuchach. Json powinien być znacznie bardziej rozsądny.
Mathieu VIALES
1
Skończyło się na tym, że stworzyłem odpowiedź, która jest „połączeniem” tego i innego, aby naprawić każdy problem z odpowiedzią (brzydota / bezpieczeństwo danych), wykorzystując mocne strony drugiej.
Mathieu VIALES
13

Tylko dla uproszczenia -

Entity Framework nie obsługuje prymitywów. Możesz utworzyć klasę, aby ją opakować, lub dodać inną właściwość, aby sformatować listę jako ciąg:

public ICollection<string> List { get; set; }
public string ListString
{
    get { return string.Join(",", List); }
    set { List = value.Split(',').ToList(); }
}
Adam Tal
źródło
1
Dzieje się tak w przypadku, gdy element listy nie może zawierać ciągu. W przeciwnym razie będziesz musiał uciec. Lub do serializacji / deserializacji listy w bardziej złożonych sytuacjach.
Adam Tal
3
Nie zapomnij też użyć [NotMapped] na nieruchomości ICollection
Ben Petersen,
7

Oczywiście Paweł udzielił właściwej odpowiedzi . Ale znalazłem w tym poście, że od EF 6+ można zapisywać prywatne nieruchomości. Więc wolałbym ten kod, ponieważ nie możesz zapisać ciągów znaków w niewłaściwy sposób.

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"
        };
    }
}
Plumpssack
źródło
6
A co jeśli ciąg zawiera przecinek?
Kredowy
4
Nie polecałbym tego robić w ten sposób. StringsAsStringszostanie zaktualizowany tylko wtedy, gdy Strings odniesienie zostanie zmienione, a jedyny przypadek w twoim przykładzie to czas przypisania. Dodanie lub usunięcie pozycji z Stringslisty po przypisaniu nie spowoduje zaktualizowania StringsAsStringszmiennej bazowej. Właściwym sposobem realizacji tego byłoby wyeksponowanie StringsAsStringsjako widoku Stringslisty, a nie odwrotnie. Połącz wartości ze sobą w getakcesorium StringsAsStringswłaściwości i podziel je w setakcesorium.
jduncanator
Aby uniknąć dodawania właściwości prywatnych (co nie jest wolne od skutków ubocznych), ustaw metodę ustawiającą zserializowaną właściwość jako prywatną. jduncanator ma oczywiście rację: jeśli nie złapiesz manipulacji listą (użyj ObservableCollection?), zmiany nie zostaną zauważone przez EF.
Leonidas,
Jak wspomniał @jduncanator, to rozwiązanie nie działa, gdy dokonywana jest modyfikacja listy (na przykład wiążąca w MVVM)
Ihab Hajj
7

Niewielkie szczypanie @Mathieu Viales „s odpowiedź , oto fragment kompatybilny NET Standardowy użyciu nowego System.Text.Json serializatora eliminując w ten sposób zależność od 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));

Zauważ, że chociaż drugi argument w obu Serialize()i Deserialize()zazwyczaj jest opcjonalny, pojawi się błąd:

Drzewo wyrażenia nie może zawierać wywołania ani wywołania używającego opcjonalnych argumentów

Jawne ustawienie wartości domyślnej (null) dla każdego usuwa to.

Xaniff
źródło
3

Możesz użyć tego ScalarCollectionkontenera, który ogranicza tablicę i zapewnia pewne opcje manipulacji ( Gist ):

Stosowanie:

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

Kod:

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);
  }
}
Shimmy Weitzhandler
źródło
8
wygląda na trochę przesadzoną ?!
Falco Alexander
1
@FalcoAlexander Zaktualizowałem swój post ... Może trochę gadatliwy, ale spełnia swoje zadanie. Upewnij się, że zastępujesz NET462odpowiednie środowisko lub dodajesz je do niego.
Shimmy Weitzhandler
1
+1 za wysiłek złożenia tego w całość. Rozwiązanie jest trochę przesadzone, jeśli chodzi o przechowywanie tablicy ciągów :)
GETah