跨方法边界的LINQ to SQL规则

时间:2012-08-13 17:56:36

标签: c# linq-to-sql

为了保持我的代码清洁,我经常尝试将LINQ to SQL中的部分数据访问代码分解为私有子方法,就像使用普通的业务逻辑代码一样。让我举一个非常简单的例子:

public IEnumerable<Item> GetItemsFromRepository()
{
    var setA = from a in this.dataContext.TableA
               where /* criteria */
               select a.Prop;

    return DoSubquery(setA);
}

private IEnumerable<Item> DoSubQuery(IEnumerable<DateTimeOffset> set)
{
     return from item in set
            where /* criteria */
            select item;
}

我确信通过更深层次的嵌套或使用集合的结果来过滤其他查询,可以想象更复杂的例子,从而无法激发任何人的想象力。

我的基本问题是:我已经看到了一些重要的性能差异,甚至只是简单地在私有方法中重新组织LINQ to SQL代码而抛出异常。任何人都可以解释这些行为的规则,以便我可以就如何编写高效,干净的数据访问代码做出明智的决定吗?

我遇到过一些问题:

1)System.Linq.Table instace到一个方法的什么时候导致查询执行?

2)在另一个查询中何时使用System.Linq.Table导致执行?

3)可以将哪些类型的操作(Take,First,Last,order by等)应用于System.Linq.Table是否有限制将参数传递给方法?

2 个答案:

答案 0 :(得分:5)

就LINQ-to-SQL而言,最重要的规则是:除非必须,否则不要返回IEnumerable<T> - 因为语义不清楚。除此之外还有两种思想流派:

  • 如果您返回IQueryable<T>,则可组合,这意味着以后查询中的where被合并为一个TSQL,但作为一个缺点,它是很难完全测试
  • 否则,返回List<T>或类似,所以很明显超出该点的所有内容都是LINQ-to-Objects

目前,你正在中间做一些事情:将它折叠到LINQ到对象(通过IEnumerable<T>),但没有明显 - 并保持连接在中间打开(再次,只是一个问题因为它不明显)

答案 1 :(得分:3)

删除隐式演员:

public IQueryable<Item> GetItemsFromRepository()
{
    var setA = from a in this.dataContext.TableA
               where /* criteria */
               select a.Prop;

    return DoSubquery(setA);
}

private IQueryable<Item> DoSubQuery(IQueryable<DateTimeOffset> set)
{
     return from item in set
            where /* criteria */
            select item;
}

IQueryable<Item>IEnumerable<Item>的隐式广告与您在AsEnumerable()上调用IQueryable<Item>基本相同。当然有时你想要这样,但是你应该默认保留IQueryable,这样整个查询就可以在数据库上执行,而不仅仅是GetItemsFromRepository()位,其余的是在记忆中完成。

次要问题:

  

1)System.Linq.Table instace到一个方法的什么时候导致查询执行?

当某些内容需要最终结果时,例如Max()ToList()等,既不是可查询对象,也不是可加载的可加载对象。

但请注意,虽然AsEnumerable()不会导致查询执行,但它确实意味着当执行确实只发生在对源数据源执行AsEnumerable()之前,这将产生一个on - 请求内存数据源,其余的将执行。

  

2)何时在另一个查询中使用System.Linq.Table导致   执行?

与上述相同。 Table<T>实施IQueryable<T>。如果你是将他们中的两个加在一起,但仍然不会导致任何事情被执行。

  

3)对什么类型的操作有限制(Take,   First,Last,order by等)可以应用于System.Linq.Table   将参数传递给方法?

IQueryable<T>确定的那些。

修改:对IEnumerableIQueryable之间的差异和相似之处进行了一些澄清。

您可以在IQueryable IEnumerable上执行任何操作,反之亦然,但它的执行方式会有所不同。

任何给定的IQueryable实现都可以在linq查询中使用,并且将包含所有linqy扩展方法,如Take()Select()GroupBy等等。

这是如何完成的,取决于实施。例如,System.Linq.Data.Table通过将查询转换为SQL查询来实现这些方法,其结果在加载的基础上转换为对象。因此,如果mySource是一个表,那么:

var filtered = from item in mySource
  where item.ID < 23
  select new{item.ID, item.Name};

foreach(var i in filtered)
  Console.WriteLine(i.Name);

变成SQL,如:

select id, name from mySourceTable where id < 23

然后从中创建一个枚举器,以便在每次调用MoveNext()时从结果中读取另一行,并从中创建一个新的匿名对象。

另一方面,如果mySource中有ListHashSet,或其他任何实现IEnumerable<T>但没有自己的查询引擎的地方,然后linq-to-objects代码将把它变成类似:

foreach(var item in mySource)
  if(item.ID < 23)
    yield return new {item.ID, item.Name};

这与代码可以在内存中完成的效率相同。结果将是相同的,但获得它们的方式将是不同的:

现在,由于所有IQueryable<T>都可以转换为等效的IEnumerable<T>,如果我们愿意,可以采用第一个mySource(在数据库中执行的地方)并执行接下来是:

var filtered = from item in mySource.AsEnumerable()
  where item.ID < 23
  select new{item.ID, item.Name};

在这里,虽然在我们遍历结果或调用检查所有结果的内容之前仍然没有对数据库执行任何操作,但一旦我们这样做,就好像我们将执行分成两个单独的步骤:

var asEnum = mySource.AsEnumerable();
var filtered = from item in asEnum
  where item.ID < 23
  select new{item.ID, item.Name};

第一行的实现是执行SQL SELECT * FROM mySourceTable,其余的执行就像上面的linq-to-objects示例。

如果数据库包含10个id&lt;的项目,那么不难看出如何23,以及50,000个id更高的物品,现在的性能要低得多。

除了提供明确的AsEnumerable()方法之外,所有IQueryable<T>都可以隐式转换为IEnumerable<T>。这允许我们对它们进行foreach并将它们与处理IEnumerable<T>的任何其他现有代码一起使用,但如果我们在不适当的时间意外执行此操作,我们可以使查询更慢,这就是当您DoSubQuery被定义为IEnumerable<DateTimeOffset>并返回IEnumerable<Item>时发生;它隐含地在AsEnumerable()IQueryable<DateTimeOffset>上调用了IQueryable<Item>,并导致数据库上可能执行的内容在内存中执行。

出于这个原因,99%的时间,我们希望继续处理IQueryable直到最后一刻。

作为相反的一个例子,只是指出AsEnumerable()IEnumerable<T>的演员阵容没有疯狂,我们应该考虑两件事。第一个是IEnumerable<T>让我们做一些其他事情无法完成的事情,比如加入两个完全不同的来源,彼此不了解(例如两个不同的数据库,一个数据库和一个XML文件等。)

另一个原因是,有时IEnumerable<T>实际上也更有效率。考虑:

IQueryable<IGrouping<string, int>> groupingQuery = from item in mySource select item.ID group by item.Name;
var list1 = groupingQuery.Select(grp => new {Name=grp.Key, Count=grp.Count()}).ToList();//fine
foreach(var grp in groupingQuery)//disaster!
  Console.WriteLine(grp.Count());

此处groupingQuery设置为可执行某些分组的查询,但无论如何都没有执行。当我们创建list1时,首先我们基于它创建一个新的IQueryable,并且查询引擎最好能够找出最适合它的SQL,并提出类似的结果:< / p>

select name, count(id) from mySourceTable group by name

这是非常有效的执行。然后将行转换为对象,然后将其放入列表中。

另一方面,对于第二个查询,对group by的SQL转换不是自然的,它不会对所有非分组项执行聚合方法,所以查询引擎可以提出的最好的方法是先做:

select distinct name from mySourceTable,

然后对于它收到的每个名字,执行:

select id from mySourceTable where name = '{name found in last query goes here}'

依此类推,这应该意味着2个SQL查询,还是200,000个。

在这种情况下,我们可以更好地处理mySource.AsEnumerable(),因为在这里 更有效率地将整个表格首先存入内存。 (更好的办法仍然是mySource.Select(item => new {item.ID, item.Name}).AsEnumerable(),因为我们仍然只从数据库中检索我们关心的列,并在那时切换到内存中。)

最后一点值得记住,因为它违反了我们的规则,我们应该尽可能长时间地待在IQueryable<T>。这不是什么值得担心的问题,但如果你进行分组并发现自己的查询速度非常慢,那么值得关注。