none
Entity Framework ObjectQuery: Include mit Where Clause RRS feed

  • Frage

  • Hallo,

    in unserer vereinfachten Datenbank sind zwei Tabllen: PERSON, BOOKING mit einer one-to-many Beziehung (eine PERSON kann mehrere BOOKING haben).

    PERSON hat eine NavigationProperty OWNERBOOKING und BOOING hat eine NavigationProperty PERSONBOOKING.

    Das ObjectContext-Objekt heisst zeusEntities.

    Das Resultat das ich von der Abfrage möchte:

    "Alle Buchungen einer einzelnen Person, die einen bestimmen Buchungstyp haben."

    Hier ist das Statement für alle Buchungen einer Person (das auch funktioniert):

    ObjectQuery<PERSON> personBookings = zeusEntities.PERSON.Where(it.PERSONID = @personId", new ObjectParameter("personId", new Guid("...")).Include("OWNERBOOKING");

    Hier ist das Statement, das ich gerne hätte, um auch die Buchungen auf einen bestimmten Buchungstyp einzuschränken:

    ObjectQuery<PERSON> personBookings = zeusEntities.PERSON.Where(it.PERSONID = @personId", new ObjectParameter("personId", new Guid("...")).Include("OWNERBOOKING").Where ("it.OWNERBOOKING.BOOKINGTYPE = 123");

    Diese Abfrage löst folgende Exception aus:

    'bookingType' is not a member of 'Transient.collection[BOOKING(Nullable=True,DefaultValue=)]'. To extract a property of a collection element, use a subquery to iterate over the collection.

    Ich arbeite mit .Net4.0, und dem VisualStudio 2010

    Benötige dringend eine Lösung für dieses Problem.

    Herzlichen Dank

    Rolf

    Mittwoch, 29. September 2010 05:30

Antworten

  • Hallo Rolf,

    Wir entfernen uns zunehmend von der Frage aus Deinem ersten Beitrag. Da diese aber geklärt ist, zurück zu meinem Codebeispiel und zu Deiner Folgefrage.

    Unter VS2008/.NET 3.5 funktionierte der obige Code, auch in einer Console-Anwendung: Der Aufruf kehrte wie erwartet zurück und es wurde keine Exception geworfen. Unter .NET 4.0 trat jedoch die von Dir erwähnte ObjectDisposedException auf und zwar beim Versuch, eine der mit zurückgegebenen Orders zu ändern. Einfache Lösung: Man entfernt das using-Statement, das den Context freigibt, und die Exception verschwindet.

    Alternativ kann man das verzögerte Laden von Entities deaktivieren:

    ObjectContextOptions options = context.ContextOptions;
    options.LazyLoadingEnabled = false;

    Wenn Du nur mit Layern arbeitest (keine Tiers, also keine physische Separation) ist das Problem gelöst. Andernfalls, in einem unverbundenen Szenario, gibt es erst einmal einiges mehr zu lesen und zu entscheiden:

    Erstellen von N-Tier-Anwendungen (Entity Framework):
    http://msdn.microsoft.com/de-de/library/bb896304.aspx

    Exemplarische Vorgehensweise: Serialisieren von Entitäten mit Selbstnachverfolgung (Entity Framework):
    http://msdn.microsoft.com/de-de/library/ee789839.aspx

    Daniel Simons - Anti-Patterns To Avoid In N-Tier Applications:
    http://msdn.microsoft.com/en-us/magazine/dd882522.aspx

     

    Solltest Du dich für binäre Serialisierung entscheiden, könnte dies in etwa so aussehen:

    class Program
    {
     static void Main(string[] args)
     {
      Program p = new Program();
      p.RunTest();
     }
    
     private void RunTest()
     {
      MemoryStream ordersStream = GetOrdersWithFreightOver40BinaryStream("ALFKI");
      ordersStream.Seek(0, SeekOrigin.Begin);
      BinaryFormatter formatter = new BinaryFormatter();
      Customers customersOrdersWithFreightOver40 = (Customers)formatter.Deserialize(ordersStream);
      customersOrdersWithFreightOver40.Orders.ElementAt(1).OrderDate = DateTime.Now;
     }
    
     private static MemoryStream GetOrdersWithFreightOver40BinaryStream(string customerID)
     {
      BinaryFormatter formatter = new BinaryFormatter();
      MemoryStream stream = new MemoryStream();
    
      using (var context = new NorthwindEntities())
      {
       try
       {
        ObjectQuery<Customers> customers = context.Customers.Where("it.CustomerID = @CustomerID", new ObjectParameter("CustomerID", customerID));
        ObjectQuery<Orders> orders = context.Orders.Where("it.Customers.CustomerID = @CustomerID AND it.Freight > 40", new ObjectParameter("CustomerID", customerID));
        customers.First().Orders.Attach(orders);
        formatter.Serialize(stream, customers.First());
       }
       catch (EntitySqlException ex)
       {
        throw new ApplicationException("The object query failed", ex);
       }
       catch (EntityCommandExecutionException ex)
       {
        throw new ApplicationException("The object query failed", ex);
       }
       catch (SerializationException ex)
       {
        throw new ApplicationException("The object graph could not be serialized", ex);
       }
      }
    
      return stream;
     }
    }
    


    Gruß
    Marcel

    Montag, 4. Oktober 2010 15:35

Alle Antworten

  • Hallo Rolf,

    Das ist so nicht möglich. Selbst wenn man das wie folgt schreiben würde - wie auch ich in einem ersten Anlauf dachte - , würde es noch immer nicht funktionieren. Ich verwende hier ein aufgrund von Northwind erstelltes Modell:

    ObjectQuery<Customers> alfkiWithOrders = context.Customers.Where("it.CustomerID='ALFKI'").Include("Orders");
    
    var query = from c in alfkiWithOrders
     where c.Orders.Any(o => o.Freight > 40)
     select c;
    
    

    Das sieht zunächst gut aus, läßt sich auch problemlos kompilieren und wirft keine Ausnahmen. Das Problem sind die Resultate.

    Wenn man sich diese nämlich z.B. in Linqpad ansieht (ein Tool das ich wärmstens empfehle), merkt man sofort dass alle relationierten Daten zurückgegeben wurden (*alle* Orders zu ALFKI). Das Problem ist konzeptioneller Natur: Die Navigationseigenschaft Orders wird immer ALLE Aufträge zurückgeben und nicht nur die wo o.Freight > 40, da wir ja EntityType Customer zurückgeben und zu dieser Entity nun einmal *alle* relationierten Orders gehören.

    Um diese Situation zu umgehen, kann man z.B. in einen anonymen Typ projizieren (hier OrdersOver40):

    var query = from c in Customers
    	 where c.CustomerID == "ALFKI"
    	 select new { c.CompanyName, 
      OrdersOver40 = 
       from o in c.Orders 
    		 where o.Freight > 40 
    		 select new {o.OrderID, o.Freight, o.ShipCity}
    		 };
    

    Das produziert folgendes SQL:

    SELECT 
    [Project1].[CustomerID] AS [CustomerID], 
    [Project1].[CompanyName] AS [CompanyName], 
    [Project1].[C1] AS [C1], 
    [Project1].[C3] AS [C2], 
    [Project1].[C2] AS [C3], 
    [Project1].[OrderID] AS [OrderID], 
    [Project1].[Freight] AS [Freight], 
    [Project1].[ShipCity] AS [ShipCity]
    FROM ( SELECT 
    	[Extent1].[CustomerID] AS [CustomerID], 
    	[Extent1].[CompanyName] AS [CompanyName], 
    	1 AS [C1], 
    	[Extent2].[OrderID] AS [OrderID], 
    	[Extent2].[Freight] AS [Freight], 
    	[Extent2].[ShipCity] AS [ShipCity], 
    	CASE WHEN ([Extent2].[OrderID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C2], 
    	CASE WHEN ([Extent2].[OrderID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C3]
    	FROM [dbo].[Customers] AS [Extent1]
    	LEFT OUTER JOIN [dbo].[Orders] AS [Extent2] ON ([Extent1].[CustomerID] = [Extent2].[CustomerID]) AND ([Extent2].[Freight] > cast(40 as decimal(18)))
    	WHERE N'ALFKI' = [Extent1].[CustomerID]
    ) AS [Project1]
    ORDER BY [Project1].[CustomerID] ASC, [Project1].[C3] ASC

    Das Ergebnis dieser Abfrage:

    Alfreds Futterkiste
     OrderID Freight ShipCity 
     10692 61,0200 Berlin
     10835 69,5300 Berlin
     10952 40,4200 Berlin
     11081 100,000 Berlin
    

    Man könnte freilich auch anders herum vorgehen und bei den Orders beginnen, um dann den relationierten Customer reinzuholen, das hängt aber davon ab, in welcher Form man die Daten haben will:

    var query = from o in Orders.Where (or => or.Customers.CustomerID == "ALFKI")
    	  where o.Freight > 40
    	  select o;
    

    oder:

    ObjectQuery<Orders> alfkiOrders = Orders.Where("it.Customers.CustomerID == 'ALFKI' AND it.Freight > 40");	

    was in Deinem Fall in etwa dieser Zeile entspricht:

    ObjectQuery<OWNERBOOKING> personBookings = zeusEntities.OWNERBOOKING.Where("it.Persons.PERSONID = 9999 AND it.BOOKINGTYPE=123");

     

    Gruß
    Marcel

    • Bearbeitet Marcel Roma Samstag, 2. Oktober 2010 18:31 Verfeinert
    Freitag, 1. Oktober 2010 18:09
  • Hallo Marcel,

    vielen Dank für Deine Hilfe. Ich habe befürchtet, dass es so nicht möglich ist. Deshalb habe ich mich in der Zwischenzeit mit Deiner Variante beschäftigt.

    Ich habe jetzt zwei Abfragen:

    ObjectQuery<PERSON> personBookings = zeusEntities.PERSON.Where(it.PERSONID = 9999);

    ObjectQuery<BOOKING> Bookings = zeusEntities.BOOKING.Where(it.OWNERBOOKINGID = 9999 and it.BOOKINGTYPE = 123)

    Wie krieg ich diese beiden Queries zusammen? Ich möchte eine Struktur der Form

    PERSON

             Buchung1

             Buchung2

             ....

    zurückgeben.

     Deshal wollte ich die erste Query personBookings um die zweite Query Bookings "ergänzen":

    foreach (BOOKING booking in Bookings) // es gibt zehn Buchungen für diese Person

    {

         personBooking.First().OWNERBOOKING.Attach(booking);

    }

    Danach möchte ich die Query mit

    List<PERSON> personBookingList = personBookings.ToList();

    return personBookingList[0]; // nur diese eine Person

    getrennt vom Context an die aufrufende Methode aus übergeordneten Geschäftslogikschicht zurückgeben. (Diese soll dort bearbeitet und an die DB-Schicht zurückgegeben werden.)

    Wenn ich in der aufrufenden Methode vom Object Typ PERSON die OWNERBOOKING bearbeite, z.B.

    personBookings.OWNERBOOKING.ElementAt(1).ApprovalStatus = 999; // es gibt zehn OwnerBooking

    wird eine ObjectDisposedException ("The ObjectContext instance has been disposed and can no longer be used for operations that require a connection") geworfen.

    Kannst Du mir hier vielleicht auch weiterhelfen?

    Gruß Rolf

    Montag, 4. Oktober 2010 08:25
  • Hallo Rolf,

    Ich kann Dir leider nur bis zu einem gewissen Punkt folgen, dort nämlich wo das Setzen des ApprovalStatus die ObjectDisposedException wirft. Wie ist denn ApprovalStatus implementiert? - Folgender Code (setzt analog das um, was ich aus Deinem Posting verstanden habe) funktioniert einwandfrei:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows.Forms;
    using System.Data.Objects;
    
    namespace NorthwindEfTest
    {
      public partial class Form1 : Form
      {
        public Form1()
        {
          InitializeComponent();
        }
    
        private void buttonGetCustomerOrdersWithFreightOver40_Click(object sender, EventArgs e)
        {
          Customers customersOrdersWithFreightOver40 = GetOrdersWithFreightOver40ForCustomer("ALFKI");
          customersOrdersWithFreightOver40.Orders.ElementAt(1).OrderDate = DateTime.Now;
        }
    
        private static Customers GetOrdersWithFreightOver40ForCustomer(string customerID)
        {
          using (var context = new NorthwindEntities())
          {
            ObjectQuery<Customers> customers = context.Customers.Where("it.CustomerID = @CustomerID", new ObjectParameter("CustomerID", customerID));
            ObjectQuery<Orders> orders = context.Orders.Where("it.Customers.CustomerID = @CustomerID AND it.Freight > 40", new ObjectParameter("CustomerID", customerID));
            customers.First().Orders.Attach(orders);
            return customers.First();
          }
        }
      }
    }
    
    

     

    Gruß
    Marcel

    Montag, 4. Oktober 2010 11:03
  • Hallo Marcel,

    danke für die rasche Antwort. Im Prinzip ist das genau das was ich möchte.

    Allerdings tritt bei mir immer noch die erwähnte Exception auf, wenn ich es so als Konsolenanwendung (so habe ich es im meinem Testprogramm) ausführe.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Data.Objects;

    namespace Northwind
    {
        class Program
        {
            private static Customers GetOrdersWithFreightOver40ForCustomer(string customerID)
            {
                using (var context = new NorthwindEntities())
                {
                    ObjectQuery<Customers> customers = context.Customers.Where("it.CustomerID = @CustomerID"new ObjectParameter("CustomerID", customerID));
                    ObjectQuery<Orders> orders = context.Orders.Where("it.Customers.CustomerID = @CustomerID AND it.Freight > 40"new ObjectParameter("CustomerID", customerID));
                    customers.First().Orders.Attach(orders);
                    return customers.First();
                }
            }

            static void Main(string[] args)
            {
                Customers customersOrdersWithFreightOver40 = GetOrdersWithFreightOver40ForCustomer("ALFKI");
                customersOrdersWithFreightOver40.Orders.ElementAt(1).OrderDate = DateTime.Now;
            }
        }
    }

    Ich weiss nicht woran es liegt. Vielleicht kannst Du es nochmals ausprobieren. Danke im voraus.

    Gruß Rolf

    Montag, 4. Oktober 2010 13:27
  • Hallo Rolf,

    Wir entfernen uns zunehmend von der Frage aus Deinem ersten Beitrag. Da diese aber geklärt ist, zurück zu meinem Codebeispiel und zu Deiner Folgefrage.

    Unter VS2008/.NET 3.5 funktionierte der obige Code, auch in einer Console-Anwendung: Der Aufruf kehrte wie erwartet zurück und es wurde keine Exception geworfen. Unter .NET 4.0 trat jedoch die von Dir erwähnte ObjectDisposedException auf und zwar beim Versuch, eine der mit zurückgegebenen Orders zu ändern. Einfache Lösung: Man entfernt das using-Statement, das den Context freigibt, und die Exception verschwindet.

    Alternativ kann man das verzögerte Laden von Entities deaktivieren:

    ObjectContextOptions options = context.ContextOptions;
    options.LazyLoadingEnabled = false;

    Wenn Du nur mit Layern arbeitest (keine Tiers, also keine physische Separation) ist das Problem gelöst. Andernfalls, in einem unverbundenen Szenario, gibt es erst einmal einiges mehr zu lesen und zu entscheiden:

    Erstellen von N-Tier-Anwendungen (Entity Framework):
    http://msdn.microsoft.com/de-de/library/bb896304.aspx

    Exemplarische Vorgehensweise: Serialisieren von Entitäten mit Selbstnachverfolgung (Entity Framework):
    http://msdn.microsoft.com/de-de/library/ee789839.aspx

    Daniel Simons - Anti-Patterns To Avoid In N-Tier Applications:
    http://msdn.microsoft.com/en-us/magazine/dd882522.aspx

     

    Solltest Du dich für binäre Serialisierung entscheiden, könnte dies in etwa so aussehen:

    class Program
    {
     static void Main(string[] args)
     {
      Program p = new Program();
      p.RunTest();
     }
    
     private void RunTest()
     {
      MemoryStream ordersStream = GetOrdersWithFreightOver40BinaryStream("ALFKI");
      ordersStream.Seek(0, SeekOrigin.Begin);
      BinaryFormatter formatter = new BinaryFormatter();
      Customers customersOrdersWithFreightOver40 = (Customers)formatter.Deserialize(ordersStream);
      customersOrdersWithFreightOver40.Orders.ElementAt(1).OrderDate = DateTime.Now;
     }
    
     private static MemoryStream GetOrdersWithFreightOver40BinaryStream(string customerID)
     {
      BinaryFormatter formatter = new BinaryFormatter();
      MemoryStream stream = new MemoryStream();
    
      using (var context = new NorthwindEntities())
      {
       try
       {
        ObjectQuery<Customers> customers = context.Customers.Where("it.CustomerID = @CustomerID", new ObjectParameter("CustomerID", customerID));
        ObjectQuery<Orders> orders = context.Orders.Where("it.Customers.CustomerID = @CustomerID AND it.Freight > 40", new ObjectParameter("CustomerID", customerID));
        customers.First().Orders.Attach(orders);
        formatter.Serialize(stream, customers.First());
       }
       catch (EntitySqlException ex)
       {
        throw new ApplicationException("The object query failed", ex);
       }
       catch (EntityCommandExecutionException ex)
       {
        throw new ApplicationException("The object query failed", ex);
       }
       catch (SerializationException ex)
       {
        throw new ApplicationException("The object graph could not be serialized", ex);
       }
      }
    
      return stream;
     }
    }
    


    Gruß
    Marcel

    Montag, 4. Oktober 2010 15:35