Gå til innhold

[Løst] EntityFramework og sletting av relaterte poster


Anbefalte innlegg

Folkens. Lurer litt på hva best practice er i dette tilfellet:

 

Sett at man har følgende entity klasser:

class Customer
{
  Public int Id{get;set;}
  Public string Name{get;set;}
  Public Virtual iqueryable<Phone> Phones{get;set;}
}

class Phone
{
  Public int Id{get;set;}
  Public string Number{get;set;}
  Public string Description{get;set;}
  Public Virtual Customer Customer{get;set;}
}

Forutse også at database siden ikke kjenner til denne relasjonen.  Det betyr at en CustomerTable.Remove(CustomerObject) ikke vil slette poster i Phone tabellen når en Customer blir slettet.

Hva er da Best Practice her?

Ville du lagt koden inn på Customer entiteten eller bør koden legges på CustomerRepository klassen ?

Koden vil da kunne se noe slik ut:

class CustomerRepository : DbContext
{
  ......
  Public Delete(Customer customer)
  {
    using (var r = New PhoneRepository())
    {
      foreach(var phone in customer.Phones)
        r.Delete(phone)
    }
    CustomerTable.Remove(customer)
  }
  .....
}

Eller finnes det faktisk en måte å fortelle repository om slike constraints?

Lenke til kommentar
Videoannonse
Annonse

Folkens. Lurer litt på hva best practice er i dette tilfellet:

 

Sett at man har følgende entity klasser:

class Customer
{
  Public int Id{get;set;}
  Public string Name{get;set;}
  Public Virtual iqueryable<Phone> Phones{get;set;}
}

class Phone
{
  Public int Id{get;set;}
  Public string Number{get;set;}
  Public string Description{get;set;}
  Public Virtual Customer Customer{get;set;}
}

Forutse også at database siden ikke kjenner til denne relasjonen.  Det betyr at en CustomerTable.Remove(CustomerObject) ikke vil slette poster i Phone tabellen når en Customer blir slettet.

Hva er da Best Practice her?

Ville du lagt koden inn på Customer entiteten eller bør koden legges på CustomerRepository klassen ?

Koden vil da kunne se noe slik ut:

class CustomerRepository : DbContext
{
  ......
  Public Delete(Customer customer)
  {
    using (var r = New PhoneRepository())
    {
      foreach(var phone in customer.Phones)
        r.Delete(phone)
    }
    CustomerTable.Remove(customer)
  }
  .....
}

Eller finnes det faktisk en måte å fortelle repository om slike constraints?

Hmm.  Lurer på om det faktisk holder å slenge på REQUIRED attributten på Phone entity:

class Phone
{
  ....
  [Required]
  Public Virtual Customer Customer{get;set;}
}

Dette vil vel strengt tatt fortelle entityframework at sletting av en CUstomer også må slette Phone records.  Vi får se ;-)

 

 

Lenke til kommentar

Ser ut som at du genererer databasen med «code first»-metoden. Det er flere muligheter, som f.eks. å sette dette opp i DbContext.OnModelCreating med «Fluent API»), men du kan bruke [Required] i dette tilfellet, som du selv har funnet ut, og dette skrur på «cascade delete». DBMS'en bør absolutt håndtere dette selv fordi det gjør det umulig å ende opp med «foreldreløse» rader selv om koden din inneholder feil.

 

Har du mer enn én DbContext? Du trenger strengt tatt bare én, spesielt i eksempelet ditt, og det gjør ting mye enklere. Ulagrede endringer gjort med én DbContext får ikke en annen DbContext og «entities/DbSets» inni der vite om. Lag heller én DbContext og putt noe som dette inn i klassen din:

public DbSet<Customer> Customers { get; set; }
public DbSet<Phone> Phones { get; set; }

Du kan fortsatt bruke «repository pattern» med én DbContext dersom du ønsker.

 

Ser forresten at du ikke bruker r.SaveChanges() i eksempelet ditt, noe jeg regner med du bevisst unnlot.

 

En siste ting: Dersom du bruker ICollection i stedet for IQueryable, kan du gjøre følgende i stedet for foreach:

customer.Phones.Clear();
Lenke til kommentar

 

Ser ut som at du genererer databasen med «code first»-metoden. Det er flere muligheter, som f.eks. å sette dette opp i DbContext.OnModelCreating med «Fluent API»), men du kan bruke [Required] i dette tilfellet, som du selv har funnet ut, og dette skrur på «cascade delete». DBMS'en bør absolutt håndtere dette selv fordi det gjør det umulig å ende opp med «foreldreløse» rader selv om koden din inneholder feil.

 

Har du mer enn én DbContext? Du trenger strengt tatt bare én, spesielt i eksempelet ditt, og det gjør ting mye enklere. Ulagrede endringer gjort med én DbContext får ikke en annen DbContext og «entities/DbSets» inni der vite om. Lag heller én DbContext og putt noe som dette inn i klassen din:

public DbSet<Customer> Customers { get; set; }
public DbSet<Phone> Phones { get; set; }

Du kan fortsatt bruke «repository pattern» med én DbContext dersom du ønsker.

 

Ser forresten at du ikke bruker r.SaveChanges() i eksempelet ditt, noe jeg regner med du bevisst unnlot.

 

En siste ting: Dersom du bruker ICollection i stedet for IQueryable, kan du gjøre følgende i stedet for foreach:

customer.Phones.Clear();

Neida.  Jeg har bare en dbcontext.  Dette var bare kode skrevet i løse luften for å illustrere.. Altså ingen grunn til å bry seg om noe som helst av det som er i eksempelkoden.

 

Men det var interresant det du sier med iCollection i stedet for iQueryable.  Var ikek klar over at en CLEAR ville gi databasen beskjed om å slette alle postene som er relatert.  Virker nesten litt skummelt, men når jeg tenker på det så gir det jo mening.

Lenke til kommentar

 

...

En siste ting: Dersom du bruker ICollection i stedet for IQueryable, kan du gjøre følgende i stedet for foreach:

customer.Phones.Clear();

...

Men det var interresant det du sier med iCollection i stedet for iQueryable.  Var ikek klar over at en CLEAR ville gi databasen beskjed om å slette alle postene som er relatert.  Virker nesten litt skummelt, men når jeg tenker på det så gir det jo mening.

 

 

Hei igjen!

 

I dag skulle jeg selv slette alle «barn» i et nytt prosjekt, og fant ut at Clear() ikke fungerte som forventet (i stedet fjernet referanser som feilet pga. FK constraint?). Det er tydelig at standard atferd (EF6) avviker fra det jeg trodde.

 

Jeg bytta til noe som tilsvarer følgende:

r.Phones.RemoveRange(customer.Phones);

Jeg beklager at jeg sa feil tidligere.

Endret av ahw_
Lenke til kommentar

 

 

...

En siste ting: Dersom du bruker ICollection i stedet for IQueryable, kan du gjøre følgende i stedet for foreach:

customer.Phones.Clear();

...

Men det var interresant det du sier med iCollection i stedet for iQueryable.  Var ikek klar over at en CLEAR ville gi databasen beskjed om å slette alle postene som er relatert.  Virker nesten litt skummelt, men når jeg tenker på det så gir det jo mening.

 

 

Hei igjen!

 

I dag skulle jeg selv slette alle «barn» i et nytt prosjekt, og fant ut at Clear() ikke fungerte som forventet (i stedet fjernet referanser som feilet pga. FK constraint?). Det er tydelig at standard atferd (EF6) avviker fra det jeg trodde.

 

Jeg bytta til noe som tilsvarer følgende:

r.Phones.RemoveRange(customer.Phones);

Jeg beklager at jeg sa feil tidligere.

 

Det går bra.  Takk for infoen.  Hadde allerede implementert første ide, men ikke gjort noe tester enda.  Kjappt for meg å endre til nevnte metodikk..

 

Takker igjen..

Lenke til kommentar

Takker igjen..

 

Det går bra.  Takk for infoen.  Hadde allerede implementert første ide, men ikke gjort noe tester enda.  Kjappt for meg å endre til nevnte metodikk..

 

OK! :)

 

Med litt triksing ser det ut som at man kan få til det jeg først foreslo (EF4):

http://www.phdesign.com.au/programming/delete-dependent-entities-when-removed-from-ef-collection/

 

Er ikke alltid at det er riktig å endre DB-strukturen, og personlig er jeg ikke en stor fan av triksing.

Endret av ahw_
Lenke til kommentar
  • 2 uker senere...

uhm, kom plutselig på noe i forbindelse med det som ble nevnt i forbindelse med multi context.  Det er jo sånn at jeg ikke har context objektet statisk.  Jeg instansierer nytt context Object hver gang jeg trenger db Access.  Er dette ikke måten å gjøre det på? Tenker på det som ble kommentert tidligere..

Min kode er noe slilk:

class RepositoryManager : DbContext
{
  // Tables
  Public DbSet<TableName> TableNameTable {get;set;}
  o.s.v.
}

Så har jeg et Interface som håndterer standard funksjoner:

public Interface IBaseRepository<T>
{
  void Add(T record);
  void Save(T record);
  void Delete(T record);
  T Fetch(int id);
  IQueryable<T> FetchAll();
}


Så for hver entitet har jeg et Interface:

Public Interface ITableNameRepository<T> : IBaseRepository<T>
{
  Spesial metoder som entiteten trenger
}

Og til slutt, selve Repository for selve entiteten:

public class TableNameRepository : RepositoryManager, ITableNameRepository<TableName>
{
  // Implementeringen av interfacene, slik som :
  public TableName Add(TableName record)
  {
    TableNameTable.Add(record);
  }
  o.s.v
}

Videre brukes dette rundt omkring i solutionen via en EntityManager class:

public class EntityManager
{
   // Alle entiterer har følgende:
   public static ITableNameReposuitory TableNames
   {
      get{Return New TableNameRepository();}
   }
}

På denne måten så forholder jeg meg kun til EntityManager rundt omkring i solutionen når jeg skal snakke med databasen.

using(var em = EntityManager.TableNames)
{
   // Kode som bruker entiteten
}

Men dette betyr jo at hver eneste kall til databasen går i en egen instans av DbContext.  Hva mener dere om dette? Er ikke dette annbefalt?

Endret av HDSoftware
Lenke til kommentar

 

 

 

...

En siste ting: Dersom du bruker ICollection i stedet for IQueryable, kan du gjøre følgende i stedet for foreach:

customer.Phones.Clear();

...

Men det var interresant det du sier med iCollection i stedet for iQueryable.  Var ikek klar over at en CLEAR ville gi databasen beskjed om å slette alle postene som er relatert.  Virker nesten litt skummelt, men når jeg tenker på det så gir det jo mening.

 

 

Hei igjen!

 

I dag skulle jeg selv slette alle «barn» i et nytt prosjekt, og fant ut at Clear() ikke fungerte som forventet (i stedet fjernet referanser som feilet pga. FK constraint?). Det er tydelig at standard atferd (EF6) avviker fra det jeg trodde.

 

Jeg bytta til noe som tilsvarer følgende:

r.Phones.RemoveRange(customer.Phones);

Jeg beklager at jeg sa feil tidligere.

 

Det går bra.  Takk for infoen.  Hadde allerede implementert første ide, men ikke gjort noe tester enda.  Kjappt for meg å endre til nevnte metodikk..

 

Takker igjen..

 

Du, vil ikke en ICollection tvinge lesing av alle dataene som er relatert ?  Er ikke poenget med en IQueryable at SQL statementet bare bygges men ikke sendes før du faktisk gjør en foreach eller ToList eller tilsvarende ?  En ICollection er jo et Interface over en Collection og den er jo dermed allerede fylt med data.  Eller vil VIRTUAL sørge for at denne også kun lastes ved behov?

Lenke til kommentar

Du, vil ikke en ICollection tvinge lesing av alle dataene som er relatert ?  Er ikke poenget med en IQueryable at SQL statementet bare bygges men ikke sendes før du faktisk gjør en foreach eller ToList eller tilsvarende ?  En ICollection er jo et Interface over en Collection og den er jo dermed allerede fylt med data.  Eller vil VIRTUAL sørge for at denne også kun lastes ved behov?

 

Kan være en god idé å se litt bort fra det jeg foreslo tidligere siden alt ikke var helt riktig, og kanskje ikke passer helt til ditt behov. Jeg har også lært litt mer siden den gang. Jeg skal prøve å forklare litt mer i detalj, men vær oppmerksom på at jeg ikke er en ekspert. Dersom ikke noe av dette kan bidra er det uansett greit for meg å ha det et sted evt. til senere.

 

Både IQueryable og ICollection er basert på IEnumerable, og støtter «deferred execution» (spørringen kjøres kun når den må, f.eks. når man kaller ToList, ja), men bare IQueryable støtter «lazy loading» også ut av boksen. Gjør du dine «properties» virtuelle, sørger EF for å lage proxy'er av dine «entities», som tar seg av «lazy loading» i stedet. Bruker man ikke «lazy loading» vil verdien være null.

 

Når du aksesserer elementene i en ICollection kjøres spørringen, men alle entity'ene lastes inn i minnet hos klienten før filtrering.

Når du aksesserer elementene i en IQueryable skjer filtreringen i selve spørringen.

 

Når dine «navigation properties» er virtuelle, lager EF en instans av en klasse som baseres på ICollection, men ikke IQueryable; av den grunn er det ikke mulig å bruke IQueryable med «navigation properties». Den eneste grunnen jeg tror det kanskje ser ut til å fungere er fordi IQueryable er basert på IEnumerable.

 

Dermed kan jeg fortsatt anbefale at dine «navigation properties» er av typen ICollection (eller IEnumerable) selv om det ikke er av nøyaktig samme grunn som sist gang. Merk at hvis du har en *:*-relasjon vil bruk av IEnumerable ikke automatisk lage tabellen for kryssreferansene.

 

Når man har mange rader bør man så klart ikke laste alt inn i minnet for deretter å forkaste mesteparten. Det finnes en løsning, men den er ikke helt optimal.

 

Her er den viktigste koden i mitt lille test-prosjekt:

using System.Data.Entity; // For DbSet<T>.Include()
// ...

    public class BookLibraryContext : DbContext
    {
        public BookLibraryContext()
            : base("name=BookLibraryContext")
        {
        }
        
        public virtual DbSet<Author> Authors { get; set; }
        public virtual DbSet<Book> Books { get; set; }
    }

    public class Author
    {
        public int AuthorId { get; set; }
        public string Name { get; set; }

        public virtual ICollection<Book> BooksAuthored { get; set; }
    }

    public class Book
    {
        public int BookId { get; set; }
        public string Title { get; set; }

        public virtual ICollection<Author> Authors { get; set; }
    }

    public class BookController : Controller
    {
        public ActionResult Details(int id)
        {
            using (var context = new BookLibraryContext()) {
                var sqlLog = new StringBuilder();
                context.Database.Log = s => {
                    Trace.Write(s);
                    sqlLog.Append(s);
                };

                var book = context.Books.SingleOrDefault(e => e.BookId == id);
                if (book == null) {
                    return HttpNotFound();
                }

                var authors = book.Authors.Take(2);

                var model = new BookDetailsViewModel {
                    BookTitle = book.Title,
                    BookAuthors = string.Join(", ", authors.Select(x => x.Name)),
                    SqlLog = sqlLog.ToString()
                };

                return View(model);
            }
        }
    }

Koden over resulterer i følgende SQL-spørringer:

Opened connection at 29.05.2015 04:47:42 +02:00
SELECT TOP (2)
[Extent1].[BookId] AS [BookId],
[Extent1].[Title] AS [Title]
FROM [dbo].[Books] AS [Extent1]
WHERE [Extent1].[BookId] = @p__linq__0
-- p__linq__0: '3' (Type = Int32, IsNullable = false)
-- Executing at 29.05.2015 04:47:42 +02:00
-- Completed in 1 ms with result: SqlDataReader

Closed connection at 29.05.2015 04:47:42 +02:00
Opened connection at 29.05.2015 04:47:42 +02:00
SELECT
[Extent2].[AuthorId] AS [AuthorId],
[Extent2].[Name] AS [Name]
FROM [dbo].[BookAuthors] AS [Extent1]
INNER JOIN [dbo].[Authors] AS [Extent2] ON [Extent1].[Author_AuthorId] = [Extent2].[AuthorId]
WHERE [Extent1].[Book_BookId] = @EntityKeyValue1
-- EntityKeyValue1: '3' (Type = Int32, IsNullable = false)
-- Executing at 29.05.2015 04:47:42 +02:00
-- Completed in 0 ms with result: SqlDataReader

Closed connection at 29.05.2015 04:47:42 +02:00

Problemet her er at Take(2) har null effekt på spørringen.

 

Ny kode:

var authors = context.Entry(book).Collection(e => e.Authors).Query().Take(2);

SQL:

Opened connection at 29.05.2015 04:49:14 +02:00
SELECT TOP (2)
[Extent1].[BookId] AS [BookId],
[Extent1].[Title] AS [Title]
FROM [dbo].[Books] AS [Extent1]
WHERE [Extent1].[BookId] = @p__linq__0
-- p__linq__0: '3' (Type = Int32, IsNullable = false)
-- Executing at 29.05.2015 04:49:14 +02:00
-- Completed in 1 ms with result: SqlDataReader

Closed connection at 29.05.2015 04:49:14 +02:00
Opened connection at 29.05.2015 04:49:14 +02:00
SELECT
[Limit1].[Name] AS [Name]
FROM ( SELECT TOP (2)
[Extent2].[Name] AS [Name]
FROM [dbo].[BookAuthors] AS [Extent1]
INNER JOIN [dbo].[Authors] AS [Extent2] ON [Extent1].[Author_AuthorId] = [Extent2].[AuthorId]
WHERE [Extent1].[Book_BookId] = @EntityKeyValue1
) AS [Limit1]
-- EntityKeyValue1: '3' (Type = Int32, IsNullable = false)
-- Executing at 29.05.2015 04:49:14 +02:00
-- Completed in 0 ms with result: SqlDataReader

Closed connection at 29.05.2015 04:49:14 +02:00

Dette gjør kanskje det du vil, men jeg synes ikke det er spesielt pent. Et annet problem er at denne enkle koden kobler til serveren hele to ganger.

 

Ny kode:

    public class BookController : Controller
    {
        public ActionResult Details(int id)
        {
            using (var context = new BookLibraryContext()) {
                var sqlLog = new StringBuilder();
                context.Database.Log = s => {
                    Trace.Write(s);
                    sqlLog.Append(s);
                };

                var container = context.Books.Where(e => e.BookId == id).Select(e => new {
                    Book = e,
                    Authors = e.Authors.Take(2)
                }).SingleOrDefault();

                if (container == null) {
                    return HttpNotFound();
                }

                var model = new BookDetailsViewModel {
                    BookTitle = container.Book.Title,
                    BookAuthors = string.Join(", ", container.Authors.Select(x => x.Name)),
                    SqlLog = sqlLog.ToString()
                };

                return View(model);
            }
        }
    }

SQL:

Opened connection at 29.05.2015 06:19:51 +02:00
SELECT 
    [Project3].[BookId] AS [BookId], 
    [Project3].[Title] AS [Title], 
    [Project3].[C1] AS [C1], 
    [Project3].[AuthorId] AS [AuthorId], 
    [Project3].[Name] AS [Name]
    FROM ( SELECT 
        [Limit1].[BookId] AS [BookId], 
        [Limit1].[Title] AS [Title], 
        [Limit2].[AuthorId] AS [AuthorId], 
        [Limit2].[Name] AS [Name], 
        CASE WHEN ([Limit2].[AuthorId] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
        FROM   (SELECT TOP (2) 
            [Extent1].[BookId] AS [BookId], 
            [Extent1].[Title] AS [Title]
            FROM [dbo].[Books] AS [Extent1]
            WHERE [Extent1].[BookId] = @p__linq__0 ) AS [Limit1]
        OUTER APPLY  (SELECT TOP (2) 
            [Extent3].[AuthorId] AS [AuthorId], 
            [Extent3].[Name] AS [Name]
            FROM  [dbo].[BookAuthors] AS [Extent2]
            INNER JOIN [dbo].[Authors] AS [Extent3] ON [Extent3].[AuthorId] = [Extent2].[Author_AuthorId]
            WHERE [Limit1].[BookId] = [Extent2].[Book_BookId] ) AS [Limit2]
    )  AS [Project3]
    ORDER BY [Project3].[BookId] ASC, [Project3].[C1] ASC
-- p__linq__0: '3' (Type = Int32, IsNullable = false)
-- Executing at 29.05.2015 06:19:51 +02:00
-- Completed in 1 ms with result: SqlDataReader

Closed connection at 29.05.2015 06:19:51 +02:00

Dette er vel kanskje det beste selv om spørringen ikke er den aller peneste.

 

Det hadde vært fint om man kunne slippe å prosjektere resultatene inn i en ny anonym klasse. I stedet for koden over hadde det vært fint å kunne gjøre noe som dette:

var book = context.Books.Include(e => e.Authors, x => x.Take(2)).SingleOrDefault(e => e.BookId == id);
// ...
var authors = book.Authors;

Noen har visst prøvd å fikse problemet selv mens MS vurderer om de vil gjøre noe med dette.

 

Du kan jo sjekke ut lenkene i kommentarene her: Allow filtering for Include extension method

Lenke til kommentar

IQueryable kan brukes på de steder du forventer å «bygge» mer på spørringen, f.eks. med mer filtrering. Hvis du f.eks. returnerer IQueryable der du burde ha returnert ICollection eller IEnumerable, risikerer du at ting som kun har med datakilden å gjøre lekker ut til f.eks. koden som kun har med UI å gjøre, eller annen logikk.

 

I værste tilfelle kan spørringen endres til noe skadelig.

Lenke til kommentar
  • 2 uker senere...

Opprett en konto eller logg inn for å kommentere

Du må være et medlem for å kunne skrive en kommentar

Opprett konto

Det er enkelt å melde seg inn for å starte en ny konto!

Start en konto

Logg inn

Har du allerede en konto? Logg inn her.

Logg inn nå
  • Hvem er aktive   0 medlemmer

    • Ingen innloggede medlemmer aktive
×
×
  • Opprett ny...