从 SQL 到 Linq






4.74/5 (19投票s)
从 SQL 到 Linq
引言
本周,我决定想知道如何在 Linq 中编写查询。我不是在谈论 Linq 扩展方法,我广泛使用过这些方法,而是使用“from
”和“select
”等关键字的查询语法。我不确定这是否会是一项特别有用的技能,因为我一直以来都只用扩展方法就过得很好,但它确实让我有点困扰,因为我从未真正掌握过 Linq 查询,也不确定语法究竟是如何工作的。我习惯于用 SQL 编写查询,所以我决定创建一些 SQL 查询,并学习如何使用 Linq 查询来编写相应的查询。本周的博客文章基本上是我这次练习的笔记。
首先,让我们明确定义。Linq 是 Language Integrated Query(语言集成查询)的缩写。它是一项 Microsoft .NET 技术,用于查询数据。而 SQL 可以用于更新和删除数据,Linq 仅用于查询数据(尽管您显然可以使用其他 .NET 命令更新和删除查询结果)。在 SQL 中,我们查询数据库表;在 Linq 中,我们查询 .NET 集合。将 SQL 与 Linq 进行比较有点奇怪,因为两者在根本上是在不同的环境中运行的,但我发现这种比较有助于我理解 Linq 查询语法。
设置数据
现在来看一些示例数据。
下方所有的 SQL 都可以在 此处找到,所有的 C# 代码都可以在 此处找到。
假设我们有两种类型的对象:Products
(产品)和 Orders
(订单)。一个 Order
(订单)与一个或多个 Products
(产品)相关联。
在 C# 中,我们的类如下所示:
public class Product
{
public int Id {get; set;}
public string Name {get; set;}
public string Description {get; set;}
public decimal Price {get; set;}
public int StockLevel {get; set;}
}
public class Order
{
public int Id {get; set;}
public IList ProductIds {get; set;}
}
或者,在 SQL 中,我们的表定义如下:
CREATE TABLE Products
(
Id INT,
Name VARCHAR(50),
Description VARCHAR(2000),
Price DECIMAL,
StockLevel INT
)
CREATE TABLE Orders
(
Id INT,
DatePlaced DATETIME
)
CREATE TABLE OrderProducts
(
Id INT,
OrderId INT,
ProductId INT
)
/* primary and foreign keys would also be defined, of course */
请注意,由于我们有一个多对多关系,所以在 SQL 中需要定义 3 个表,但在 C# 中只需要定义 2 个实体。
现在来看实际数据。
C#
public static IList<Product> GetProducts()
{
var products = new List<Product>();
products.Add(new Product
{
Id = 1,
Name = "Sausages",
Description = "Succulent pork sausages, from the finest farms in the land",
Price = 4.99M,
StockLevel = 12
});
products.Add(new Product
{
Id = 2,
Name = "Bacon",
Description = "Delicious bacon, fry it or grill it!",
Price = 3.50M,
StockLevel = 5
});
products.Add(new Product
{
Id = 3,
Name = "Chicken Fillets",
Description = "Our chickens are treated well, and our fillets will treat you well",
Price = 8.99M,
StockLevel = 4
});
products.Add(new Product
{
Id = 4,
Name = "Fishcakes",
Description = "If you love fish and you love cakes then you'll love our fish cakes",
Price = 3.99M,
StockLevel = 22
});
products.Add(new Product
{
Id = 5,
Name = "Lamb Chops",
Description = "Lovely with mint sauce",
Price = 12.00M,
StockLevel = 0
});
return products;
}
public static IList<Orders> GetOrders()
{
var orders = new List<Order>();
orders.Add(new Order
{
Id = 1,
ProductIds = new List(){ 1, 4 }
});
orders.Add(new Order
{
Id = 2,
ProductIds = new List(){ 2, 3, 5 }
});
orders.Add(new Order
{
Id = 3,
ProductIds = new List(){ 3, 1 }
});
orders.Add(new Order
{
Id = 4,
ProductIds = new List(){ 4 }
});
orders.Add(new Order
{
Id = 5,
ProductIds = new List(){ 2, 4, 3, 1 }
});
return orders;
}
SQL
INSERT INTO Products(Id, Name, Description, Price, StockLevel)
VALUES (1, 'Sausages', 'Succulent pork sausages, from the finest farms in the land', 4.99, 12)
INSERT INTO Products(Id, Name, Description, Price, StockLevel)
VALUES (2, 'Bacon', 'Delicious bacon, fry it or grill it!', 3.50, 5)
INSERT INTO Products(Id, Name, Description, Price, StockLevel)
VALUES (3, 'Chicken Fillets', 'Our chickens are treated well, _
and our fillets will treat you well', 8.99, 4)
INSERT INTO Products(Id, Name, Description, Price, StockLevel)
VALUES (4, 'Fishcakes', _
'If you love fish and you love cakes then you''ll love our fish cakes', 3.99, 22)
INSERT INTO Products(Id, Name, Description, Price, StockLevel)
VALUES (5, 'Lamb Chops', 'Lovely with mint sauce', 12.00, 0)
INSERT INTO Orders (Id, DatePlaced) VALUES (1, '1 January 2015')
INSERT INTO Orders (Id, DatePlaced) VALUES (2, '1 February 2015')
INSERT INTO Orders (Id, DatePlaced) VALUES (3, '12 February 2015')
INSERT INTO Orders (Id, DatePlaced) VALUES (4, '29 March 2015')
INSERT INTO Orders (Id, DatePlaced) VALUES (5, '1 July 2015')
INSERT INTO OrderProducts(Id, OrderId, ProductId) VALUES (1, 1, 4)
INSERT INTO OrderProducts(Id, OrderId, ProductId) VALUES (2, 1, 1)
INSERT INTO OrderProducts(Id, OrderId, ProductId) VALUES (3, 2, 2)
INSERT INTO OrderProducts(Id, OrderId, ProductId) VALUES (4, 2, 3)
INSERT INTO OrderProducts(Id, OrderId, ProductId) VALUES (5, 2, 5)
INSERT INTO OrderProducts(Id, OrderId, ProductId) VALUES (6, 3, 3)
INSERT INTO OrderProducts(Id, OrderId, ProductId) VALUES (7, 3, 1)
INSERT INTO OrderProducts(Id, OrderId, ProductId) VALUES (8, 4, 4)
INSERT INTO OrderProducts(Id, OrderId, ProductId) VALUES (9, 5, 2)
INSERT INTO OrderProducts(Id, OrderId, ProductId) VALUES (10, 5, 4)
INSERT INTO OrderProducts(Id, OrderId, ProductId) VALUES (11, 5, 3)
INSERT INTO OrderProducts(Id, OrderId, ProductId) VALUES (12, 5, 1)
请注意,我在 Order
类中使用了 ProductIds
集合,而不是 Products
集合。这纯粹是为了能够探索 Linq 的 join
(连接)功能。
现在我们来看看查询数据的几个最常见用例。
选择所有内容
SQL
SELECT * FROM Products
C#
var allProducts = from p in products
select p;
这里没有什么特别有趣的。在 Linq 中,select
部分放在查询的末尾,而不是开头。这对我来说不太直观,因为整个查询读起来不如其 SQL 等价物那样像一句自然的英语句子。
选择特定列
SQL
SELECT Id, Name FROM Products
C#
var idAndNameOnly = from p in products
select new { p.Id, p.Name };
这种情况显示了 Linq 和 SQL 之间的一些固有差异。Linq 是围绕面向对象设计的。在 SQL 中,您可以轻松指定要返回的列,因为您的结果始终是 dataset
(数据集)。由于 Linq 是 .NET 的一部分,而 .NET 是一个面向对象的框架,因此其查询返回的是对象的集合(集合本身也是对象)。因此,如果我们只想要产品 ID 和名称,我们就必须做一些奇怪的事情,比如使用此处所示的匿名类型表示法。实际上,您极少会以这种方式使用 C#,而是会返回完整的 Product
(产品)对象,然后只访问您需要的任何属性。然而,如果返回的对象很大且复杂,而您只需要访问一两个属性,这可能会导致性能问题。
当我们选择的不是正在查询的实际对象时,这被称为 **projection**(投影)。
过滤(WHERE)
SQL
SELECT * FROM Products WHERE StockLevel < 5
C#
var lowStock = from p in products
where p.StockLevel < 5
select p;
足够直接。
排序
SQL
SELECT * FROM Products ORDER BY Price DESC
C#
var byPrice = from p in products
orderby p.Price descending
select p;
同样,这里也没有太大区别。
聚合函数
SQL
SELECT SUM(Price) FROM Products
SELECT COUNT(*) FROM Products
SELECT MIN(StockLevel) FROM Products
C#
var priceSum = products.Sum(p => p.Price);
var productCount = products.Count();
var minStockLevel = products.Min(p => p.StockLevel);
关于聚合函数,我有些惊讶地发现,没有办法在不使用扩展方法的情况下获得所需的结果。这是因为纯粹从查询语法构建的语句始终返回一个集合,而聚合函数返回一个单一值,因此需要一个扩展方法。在 SQL 中,查询始终返回一个 dataset
(数据集)。如果您的 SQL 查询返回一个单一值,那么它实际上返回一个由一列和一行组成的 dataset
(数据集)。
连接
SQL
SELECT * FROM Products p
INNER JOIN OrderProducts op
ON p.Id = op.ProductId
INNER JOIN Orders o
ON o.Id = op.OrderId
C#
var orderProducts = from o in orders
select new
{
Order = o,
Products = from p in products
join pid in o.ProductIds
on p.Id equals pid
select p
};
由于 Orders
(订单)和 Products
(产品)之间存在多对多关系,我们需要创建一个内部查询才能获得与 Order
(订单)相关的所有 Products
(产品)。当我们在 Linq 中使用 join
(连接)关键字时,必须使用 equals
(等于)运算符。
选择唯一值(Distinct)
SQL
SELECT DISTINCT Price FROM Products
C#
var distinctPrices = (from p in products
select p.Price).Distinct();
同样,在 Linq 中,我们需要使用扩展方法来检索唯一值。
选择第一个 / 前 n 条结果
SQL
SELECT TOP 3 * FROM Products ORDER BY Price DESC
C#
var top3Expensive = (from p in products
orderby p.Price descending
select p).Take(3);
最后,要仅检索“前”n 条结果,我们再次需要使用扩展方法。
结论
现在已经学习了 Linq 查询语法的基础知识,我必须承认我并不喜欢它。首先,我觉得 Linq 语法不如 SQL 直观。它似乎与英语句子的结构不太匹配。其次,在 C# 中编写这种代码感觉很不自然。Linq 扩展方法不仅易于阅读和编写,而且更符合 C# 的习惯用法。凭我记忆,我想不出任何不被扩展方法支持的场景。
以上所有查询都仅使用扩展方法编写:
var allProductsExt = products.Select(p => p);
var idAndNameOnlyExt = products.Select(p => new { p.Id, p.Name });
var lowStockExt = products.Where(p => p.StockLevel < 5);
var byPriceExt = products.OrderByDescending(p => p.Price);
var orderProductsExt = orders.Select(o => new
{ Order = o, Products = products.Where(p => o.ProductIds.Contains(p.Id))});
var distinctPricesExt = products.Select(p => p.Price).Distinct();
var top3ExpensiveExt = products.OrderByDescending(p => p.Price).Take(3);
因此,这可能是我最后一次使用 Linq 查询语法编写查询了。不过,探索一下还是很有趣的。
这篇博文 From SQL to Linq 最初发布在 The Proactive Programmer。