Entity Framework - Code First - ไม่สามารถจัดเก็บรายการ <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
คุณคาดหวังว่ารายการ <sting> จะถูกเก็บไว้ในฐานข้อมูลได้อย่างไร? ไม่ได้ผล เปลี่ยนเป็นสตริง
Wiktor Zychla

4
หากคุณมีรายการจะต้องชี้ไปที่เอนทิตีบางอย่าง เพื่อให้ EF จัดเก็บรายการจำเป็นต้องมีตารางที่สอง ในตารางที่สองจะใส่ทุกอย่างจากรายการของคุณและใช้คีย์นอกเพื่อชี้กลับไปที่Testเอนทิตีของคุณ ดังนั้นสร้างเอนทิตีใหม่ที่มีIdคุณสมบัติและMyStringคุณสมบัติจากนั้นทำรายการนั้น
Daniel Gabriel

1
ถูกต้อง ... มันไม่สามารถเก็บไว้ใน db โดยตรง แต่ฉันหวังว่า Entity Framework จะสร้างเอนทิตีใหม่เพื่อทำสิ่งนั้นด้วยตัวเอง ขอบคุณสำหรับความคิดเห็น
พอล

คำตอบ:


162

Entity Framework ไม่สนับสนุนคอลเลกชันของชนิดดั้งเดิม คุณสามารถสร้างเอนทิตี (ซึ่งจะถูกบันทึกลงในตารางอื่น) หรือดำเนินการประมวลผลสตริงเพื่อบันทึกรายการของคุณเป็นสตริงและเติมข้อมูลในรายการหลังจากเอนทิตีเป็นรูปธรรม


จะเกิดอะไรขึ้นหากเอนทิตีมีรายการเอนทิตี จะบันทึกการทำแผนที่ได้อย่างไร
A_Arnold

ขึ้นอยู่กับ - มักจะเป็นตารางแยกต่างหาก
Pawel

สามารถลองทำให้เป็นอนุกรมแล้วบีบอัดและบันทึกข้อความที่จัดรูปแบบ json หรือเข้ารหัสและบันทึกหากจำเป็น ไม่ว่าจะด้วยวิธีใดก็ตามที่คุณไม่มีเฟรมเวิร์กทำการแมปตารางชนิดที่ซับซ้อนให้คุณ
Niklas

100

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 (ถ่านใหม่ [] {';'}, StringSplitOptions.RemoveEmptyEntries));
Peter Koller

8
นี่เป็นคำตอบเดียวที่ถูกต้องจริงๆ IMHO คนอื่น ๆ ทั้งหมดต้องการให้คุณเปลี่ยนโมเดลของคุณและนั่นเป็นการละเมิดหลักการที่ว่าโมเดลโดเมนควรจะคงอยู่โดยไม่รู้ตัว (เป็นเรื่องดีถ้าคุณใช้การคงอยู่และแบบจำลองโดเมนแยกกัน แต่มีเพียงไม่กี่คนที่ทำเช่นนั้น)
Marcell Toth

2
คุณควรยอมรับคำขอแก้ไขของฉันเพราะคุณไม่สามารถใช้ char เป็นอาร์กิวเมนต์แรกของสตริงได้เข้าร่วมและคุณต้องระบุ char [] เป็นอาร์กิวเมนต์แรกของสตริงแยกหากคุณต้องการให้ StringSplitOptions ด้วย
Dominik

2
ใน. NET Core คุณสามารถทำได้ ฉันใช้โค้ดชิ้นนี้ในโปรเจ็กต์หนึ่งของฉัน
Sasan

2
ไม่มีใน. NET Standard
Sasan

61

คำตอบนี้จะขึ้นอยู่กับคนที่มีให้โดย@Sasanและ@CAD เจ้าหมอ

หากคุณต้องการใช้สิ่งนี้ใน. NET Standard 2 หรือไม่ต้องการให้ Newtonsoft ดูคำตอบของ Xaniffด้านล่าง

ใช้งานได้กับ 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 ) แต่เป็นคำตอบเดียวที่ปลอดภัยต่อข้อมูล

9
ไชโยนี่น่าจะเป็นคำตอบที่ได้รับการยอมรับ
Shirkan

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

42

ฉันรู้ว่านี่เป็นคำถามเก่าและ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]ดังนั้นจึงเห็นได้ชัดมากสำหรับฉันว่า "นี่ไม่ใช่คุณสมบัติที่คุณกำลังมองหา" ในการเขียนโค้ดตามปกติ คุณสมบัติ "จริง" ถูกแท็กด้วย[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

12

เพียงเพื่อให้ง่ายขึ้น -

เอนทิตีเฟรมเวิร์กไม่รองรับแบบดั้งเดิม คุณสร้างคลาสเพื่อตัดหรือเพิ่มคุณสมบัติอื่นเพื่อจัดรูปแบบรายการเป็นสตริง:

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

1
ในกรณีที่รายการไม่สามารถมีสตริงได้ มิฉะนั้นคุณจะต้องหลบหนี หรือเพื่อทำให้เป็นอนุกรม / deserialize รายการสำหรับสถานการณ์ที่ซับซ้อนมากขึ้น
Adam Tal

3
นอกจากนี้อย่าลืมใช้ [NotMapped] กับคุณสมบัติ ICollection
Ben Petersen

12

ปรับแต่งคำตอบของ@Mathieu Vialesเล็กน้อยนี่คือตัวอย่างข้อมูลที่เข้ากันได้กับ.

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) อย่างชัดเจนสำหรับการล้างข้อมูลนั้น


1
ฉันกำลังคิดที่จะอัปเดตคำตอบเพื่อเพิ่มทางเลือกใหม่ในการทำให้เป็นอนุกรม ฉันชอบการใช้ defaultเคล็ดลับ params เสริมสำหรับคุณ ทำให้รหัสสะอาดมาก
Mathieu VIALES

6

แน่นอนPawel ได้ให้คำตอบที่เหมาะสม แต่ฉันพบในโพสต์นี้ว่าเนื่องจาก EF 6+ สามารถบันทึกคุณสมบัติส่วนตัวได้ ดังนั้นฉันจึงต้องการรหัสนี้เนื่องจากคุณไม่สามารถบันทึก Strings ได้ผิดวิธี

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

7
จะเกิดอะไรขึ้นถ้าสตริงมีลูกน้ำ?
Chalky

4
ฉันไม่แนะนำให้ทำด้วยวิธีนี้ StringsAsStringsจะอัปเดตเมื่อStrings ข้อมูลอ้างอิงมีการเปลี่ยนแปลงเท่านั้นและครั้งเดียวในตัวอย่างของคุณที่เกิดขึ้นคือการมอบหมายงาน การเพิ่มหรือลบรายการออกจากรายการของคุณStringsหลังการมอบหมายจะไม่อัปเดตStringsAsStringsตัวแปรสำรอง วิธีที่เหมาะสมในการนำไปใช้คือการแสดงStringsAsStringsเป็นมุมมองของStringsรายการแทนที่จะเป็นวิธีอื่น ๆ รวมค่าเข้าด้วยกันในgetaccessor ของStringsAsStringsคุณสมบัติและแยกค่าในsetaccessor
jduncanator

เพื่อหลีกเลี่ยงการเพิ่มคุณสมบัติส่วนตัว (ซึ่งไม่ใช่ผลข้างเคียงฟรี) ทำให้ตัวตั้งค่าของคุณสมบัติที่ทำให้เป็นอนุกรมเป็นแบบส่วนตัว แน่นอนว่า jduncanator นั้นถูกต้อง: หากคุณไม่จับการปรับแต่งรายการ (ใช้ ObservableCollection?) EF จะไม่สังเกตเห็นการเปลี่ยนแปลง
Leonidas

ดังที่ @jduncanator กล่าวว่าโซลูชันนี้ใช้ไม่ได้เมื่อมีการปรับเปลี่ยนรายการ (เช่นการผูกใน MVVM)
Ihab

2

คุณสามารถใช้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);
  }
}

10
ดูดีไปหน่อย?!
Falco Alexander

1
@FalcoAlexander ฉันอัปเดตโพสต์ของฉัน ... อาจจะดูละเอียดไปหน่อย แต่ได้ผล ตรวจสอบให้แน่ใจว่าคุณได้แทนที่NET462ด้วยสภาพแวดล้อมที่เหมาะสมหรือเพิ่มเข้าไป
Shimmy Weitzhandler

1
+1 สำหรับความพยายามในการรวบรวมสิ่งนี้ วิธีแก้ปัญหานี้ค่อนข้างมากเกินไปสำหรับการจัดเก็บอาร์เรย์ของสตริง :)
GETah

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.