4

I have the following Code First models (though in fact I'm just using EF to access a subset of an existing database; Table & Column attributes have been omitted from this example for brevity):

public enum AlphaState { Unknown = '\0', Good = 'G', Bad = 'B' }

public class Alpha
{
    [Key]
    public long AlphaIndex { get; set; }

    public string Deleted { get; set; }
    public AlphaState State { get; set; }

    [InverseProperty("Alpha")]
    public ICollection<Bravo> Bravos { get; set; }
}

public class Bravo
{
    [Key]
    public long BravoIndex { get; set; }

    [ForeignKey("Alpha")]
    public long? AlphaIndex { get; set; }
    public virtual Alpha Alpha { get; set; }

    [InverseProperty("Bravo")]
    public ICollection<Charlie> Charlies { get; set; }
}

public class Charlie
{
    [Key]
    public int CharlieIndex { get; set; }

    public string Deleted { get; set; }
    public DateTime CreatedAt { get; set; }

    [ForeignKey("Bravo")]
    public long BravoIndex { get; set; }
    public virtual Bravo Bravo { get; set; }

    [ForeignKey("Delta")]
    public long DeltaIndex { get; set; }
    public virtual Delta Delta { get; set; }

    [InverseProperty("Charlie")]
    public virtual ICollection<Delta> AllDeltas { get; set; }
}

public class Delta
{
    [Key]
    public long DeltaIndex { get; set; }

    [ForeignKey("Charlie")]
    public long CharlieIndex { get; set; }
    public virtual Charlie Charlie { get; set; }

    [InverseProperty("Delta")] // actually a 1:0..1 relationship
    public ICollection<Echo> Echoes { get; set; }
}

public enum EchoType { Unknown = 0, One = 1, Two = 2, Three = 3 }

public class Echo
{
    [Key]
    public int EchoIndex { get; set; }

    public EchoType Type { get; set; }

    [ForeignKey("Delta")]
    public long DeltaIndex { get; set; }
    public virtual Delta Delta { get; set; }
}

When I attempt this query:

var result = context.Alphas.Where(a => a.State == AlphaState.Good)
                           .Where(a => a.Deleted != "Y")
                           .Where(a => a.Bravos.SelectMany(b => b.Charlies)
                                               .Where(c => c.Deleted != "Y")
                                               .Where(c => c.Delta.Echoes.Any())
                                               .OrderByDescending(c => c.CreatedAt).Take(1)
                                               .Any(c => c.Delta.Echoes.Any(e => e.Type == EchoType.Two)))
                           .Select(a => a.AlphaIndex);

...or this equivalent query (B->A and D->E are 1:0..1 relations):

var result = context.Bravos.Where(b => b.Alpha != null)
                           .Where(b => b.Alpha.State == AlphaState.Good)
                           .Where(b => b.Alpha.Deleted != "Y")
                           .Where(b => b.Charlies.Where(c => c.Deleted != "Y")
                                                 .Where(c => c.Delta.Echoes.Any())
                                                 .OrderByDescending(c => c.CreatedAt)
                                                 .FirstOrDefault().Delta.Echoes
                                                 .Any(e => e.Type == EchoType.Two))
                           .Select(b => b.AlphaIndex);

...I get an exception because of a problem in the SQL statement that is generated. Specifically, it's trying to use the declared identifier for the Alpha table in a multi-nested subquery, and, as described here, Oracle doesn't map identifiers more than 1 level deep.

Am I crazy, or missing something obvious? Did Oracle seriously ship an EF provider that will generate queries their own database can't run for reasonable (though admittedly not trivial) queries?

Is there a configuration option that can be set (in the database or in EF) that will help with this? Is there some strategy I can use to build queries differently or split them up that will solve this problem (without tanking performance by turning a single round-trip into many)?

Note that I can write a SQL statement myself that will get the same information:

select A.alpha_index
from ALPHA A
inner join BRAVO B on B.alpha_index = A.alpha_index
inner join CHARLIE C on C.bravo_index = B.bravo_index
inner join ECHO E on E.delta_index = D.delta_index
where (A.deleted is null or A.deleted <> 'Y')
  and A.state = 'G'
  and E.type = 2
  and C.created_at = (select max(C2.created_at)
                      from CHARLIE C2
                      inner join ECHO E2 on E2.delta_index = C2.delta_index
                      where (C2.deleted is null or C2.deleted <> 'Y')
                        and C2.bravo_index = C.bravo_index)

I want to use EF to make a modular system that allows users who don't know any SQL to build their own queries using building blocks I provide; each "block" would have an Expression<Func<Model, bool>> that can be slapped into a chain of Where clauses to build the query. The whole point of this is to avoid having to write actual SQL queries for everything users might want to look for.

Community
  • 1
  • 1
Oblivious Sage
  • 3,326
  • 6
  • 37
  • 58
  • I've had similar trouble with the Oracle EF Provider where the generated SQL didn't comply with the [30 character limit](http://stackoverflow.com/questions/1378133/why-are-oracle-table-column-index-names-limited-to-30-characters). – Maarten Jun 09 '15 at 07:20

1 Answers1

0

The problem is not really Oracle. EF Provider for Oracle generates small pieces of queries that EF ask to it. EF, in this, is not efficient.
Anyway, in this case you have 2 different approaches.
You could try to simplify the query (like you wrote).
The other approach is to use a different mapper that starts from SQL query (not LINQ queries) to materialize entities. I know Dapper but actually is good only for read only purpose and has several other limitations.

I don't know what is the worst...

bubi
  • 6,414
  • 3
  • 28
  • 45
  • I'm basically prototyping a structure to allow users who don't know SQL to build queries using a building-block UI, so read-only isn't a problem for me. Would Dapper allow me to combine multiple Where conditions? In EF I would have just saved an Expression> for each building block, would I be able to do something like that with Dapper? Also, does it work with SQL Server? – Oblivious Sage Jun 09 '15 at 13:02
  • @ObliviousSage: Dapper does work with SQL Server! As a matter of fact, it was written by our gracious hosts here at SO to connect to SQL Server. – Gabe Jun 09 '15 at 14:20
  • @Gabe On further investigation I don't think Dapper is what I'm looking for: it's the wrong "half" of an ORM. It won't write the query for you, it just maps the results into POCOs; I, on the other hand, need something that does the opposite. I'd like something that can build the query for me from a bunch of interchangeable pieces I can save, but I don't actually need the results in POCOs. – Oblivious Sage Jun 09 '15 at 15:08
  • @ObliviousSage in this case Dapper is not suitable. There are several mappers that allows linq queries but I used only NHibernate (since some months ago I'm using EF) and usually NHibernate keeps query more simple but you need to materialize the query often because of NH limitations. – bubi Jun 09 '15 at 15:16
  • OK, I'll look into NHibernate as well. Thanks bubi! – Oblivious Sage Jun 09 '15 at 15:17