实战项目-连接查询
链接查询
1. 在 DAO 层进行单表查询,Service 层调用两个 DAO 并进行多表查询后封装
优点:
- 代码复用性:单表查询的方法可以在不同的业务场景中复用。
- 职责分离:每个 DAO 负责单一的数据表操作,保持了职责的清晰和单一。
- 测试方便:单表查询的方法更容易进行单元测试。
- 灵活性:可以根据需要灵活组合不同的 DAO 方法来满足复杂的业务需求。
缺点:
- 多次数据库访问:可能会导致多次数据库访问,增加了网络延迟和资源消耗。
- 事务管理复杂:如果需要跨多个 DAO 的事务管理,可能会增加复杂性。
- 性能问题:多次查询可能会影响性能,特别是在大数据量的情况下。
2. 直接在 DAO 层进行多表查询并封装 VO(视图对象)
优点:
- 减少数据库访问:一次查询可以获取所有需要的数据,减少了网络延迟和资源消耗。
- 性能优化:通过减少数据库访问次数,可以显著提高性能。
- 事务管理简单:在一个事务中完成所有操作,简化了事务管理。
- 封装性好:可以直接返回一个封装好的 VO 对象,减少了服务层的处理逻辑。
缺点:
- 代码复用性较低:多表查询的方法通常更特定于某个业务场景,复用性较低。
- DAO 层复杂度增加:DAO 层的代码会变得更加复杂,因为需要处理多表关联和数据封装。
- 测试难度增加:多表查询的方法可能更难进行单元测试,尤其是当涉及到复杂的 SQL 查询时。
- 耦合性较高:DAO 层直接封装了多个表的数据,可能会导致更高的耦合性。
LINQ 查询
public DishPageResp GetPage(DishPageQueryReq dishPageQueryReq,
bool isAsNoTracking = true)
{
Queryable<xiaobai_cangqiong_Storage.Entity.Dish> pagedDishes = _dbContext.Dishes;
if (isAsNoTracking)
{
pagedDishes = pagedDishes.AsNoTracking();
}
// 拼接查询条件
if (dishPageQueryReq.Name != null)
{
pagedDishes = pagedDishes.Where(d => d.Name.Contains(dishPageQueryReq.Name));
}
if (dishPageQueryReq.CategoryId != 0)
{
pagedDishes = pagedDishes.Where(d => d.CategoryId == dishPageQueryReq.CategoryId);
}
if (dishPageQueryReq.Status != null)
{
pagedDishes = pagedDishes.Where(d => d.Status == dishPageQueryReq.Status);
}
var total = pagedDishes.Count();
// 分页查询(以创建时间排序)
pagedDishes = pagedDishes.OrderBy(d => d.CreateTime)
.Skip((dishPageQueryReq.Page - 1) * dishPageQueryReq.PageSize)
.Take(dishPageQueryReq.PageSize);
// 链接查询
var dishPageVos = pagedDishes.Join(_dbContext.Categories, dish => dish.CategoryId, category => category.Id,
(dish, category) => new DishPageVo
{
Id = dish.Id,
Name = dish.Name,
CategoryId = dish.CategoryId,
Price = dish.Price,
Image = dish.Image,
Description = dish.Description,
Status = dish.Status,
UpdateTime = dish.UpdateTime,
CategoryName = category.Name
});
return new DishPageResp
{
Total = total,
Records = dishPageVos.ToList(),
};
}
内连接查询
在这里,我们链接查询后,手动将结果集映射到VO对象中
在这种对一关系中,我们可以直接简单的进行内连接查询,因为内连接查询效率最高
当有一个菜品的分类为空时,则需要用到外连接查询,但在咱们的项目中没有分类为空的菜品,所以没有这种需求
var dishPageVos = pagedDishes.Join(_dbContext.Categories, dish => dish.CategoryId, category => category.Id,
(dish, category) => new DishPageVo
{
Id = dish.Id,
Name = dish.Name,
CategoryId = dish.CategoryId,
Price = dish.Price,
Image = dish.Image,
Description = dish.Description,
Status = dish.Status,
UpdateTime = dish.UpdateTime,
// 链接查询分类名称
CategoryName = category.Name
});
外连接查询
在这里,我们实际项目中的菜品数据是必须存在于分类的,那么使用链接查询即可完成业务需求
但如果存在菜品是没有被分类的,则需要用到外连接查询
GroupJoin
- 将主表(如
pagedDishes
)中的每个元素与从表(如_dbContext.Categories
)中的元素进行分组连接。 - 结果是一个匿名对象,其中包含主表中的每个元素和一个从表中匹配的元素集合(如果有的话)。
SelectMany
- 使用
SelectMany
对GroupJoin
的结果进行展平。 - 如果某个主表元素在从表中没有匹配项,则使用
DefaultIfEmpty()
方法提供一个默认值(通常为null
)。 - 最终结果是一个扁平化的集合,包含了所有主表元素及其对应的从表元素(如果有)或默认值(如果没有匹配项)。
var dishPageVos = pagedDishes
// 使用 GroupJoin 进行分组连接
.GroupJoin(
_dbContext.Categories, // 要连接的类别集合
dish => dish.CategoryId, // 从每个 dish 中提取 CategoryId
category => category.Id, // 从每个 category 中提取 Id
(dish, categoriesGroup) => new { Dish = dish, CategoriesGroup = categoriesGroup.DefaultIfEmpty() } // 结果选择器
)
// 使用 SelectMany 展平结果,并处理没有匹配 Category 的情况
.SelectMany(
x => x.CategoriesGroup, // 如果没有匹配的 Category,则返回默认值(null)
(x, c) => new DishVo // 投影结果到 DishPageVo 对象
{
Id = x.Dish.Id, // 设置 DishPageVo 的 Id 属性为 dish 的 Id
Name = x.Dish.Name, // 设置 DishPageVo 的 Name 属性为 dish 的 Name
CategoryId = x.Dish.CategoryId, // 设置 DishPageVo 的 CategoryId 属性为 dish 的 CategoryId
Price = x.Dish.Price, // 设置 DishPageVo 的 Price 属性为 dish 的 Price
Image = x.Dish.Image, // 设置 DishPageVo 的 Image 属性为 dish 的 Image
Description = x.Dish.Description, // 设置 DishPageVo 的 Description 属性为 dish 的 Description
Status = x.Dish.Status, // 设置 DishPageVo 的 Status 属性为 dish 的 Status
UpdateTime = x.Dish.UpdateTime, // 设置 DishPageVo 的 UpdateTime 属性为 dish 的 UpdateTime
// 如果 category 为 null,则将 CategoryName 设置为 "未分类",否则设置为 category 的 Name
CategoryName = c != null ? c.Name : "未分类"
}
).ToList();
代码详解:
在进行.GroupJoin
分组查询之后,产生的结果集(x)为一个元素(Dish)和其关联的结果集(CategoriesGroup)的集合
.SelectMany
的第一个参数为x => x.CategoriesGroup
,其含义是将其关联的结果集(CategoriesGroup)展平
这里的两个参数(x, category)
,x为结果集,category为 关联的结果集(CategoriesGroup)的元素中的一个 Category
对象,或者 null
(如果没有匹配的 Category
)
一对多关系处理
在Mybatis中,我们使用resultMap和collection来完成一对多的关系处理
在使用GroupJoin方法进行外连接查询后,第一个类型中的每一个元素都包含一个第二个类型的一个元素集合
我们使用SelectMany的方式展开,就实现了数据库左外连接查询的结果
所以通过一个简单的GroupJoin配合Select,就在ASP.NETCore中完成了一对多关系的处理
在这里,我们不需要将dishFlavor结果集展平,因为我们需要的是Flavor(List<DishFlavor>)集合,所以直接ToList为其赋值即可
IQueryable<xiaobai_cangqiong_Storage.Entity.Dish> queryable = _dbContext.Dishes;
queryable.GroupJoin(_dbContext.DishFlavors
, d => d.Id
, d => d.DishId
, (dish, dishFlavor) => new { Dish = dish, DishFlavor = dishFlavor.DefaultIfEmpty() })
.Select(x => new DishListResp
{
Id = x.Dish.Id,
Name = x.Dish.Name,
CategoryId = x.Dish.CategoryId,
Price = x.Dish.Price,
Image = x.Dish.Image,
Description = x.Dish.Description,
Status = x.Dish.Status,
UpdateTime = x.Dish.UpdateTime,
Flavors = x.DishFlavor.ToList(),
});
LINQ表达式语法
我们发现,在进行多表的连接查询时,LINQ的方法语法会比表达式语法更加麻烦
再表达式语法中,如果进行外连接查询,会自动处理Flavors = flavors.DefaultIfEmpty(),且这种写法简单,更符合SQL语义
// 使用 Join 进行内连接
var dishPageVos = from dish in pagedDishes
join category in _dbContext.Categories on dish.CategoryId equals category.Id
select new DishPageVo
{
Id = dish.Id,
Name = dish.Name,
CategoryId = dish.CategoryId,
Price = dish.Price,
Image = dish.Image,
Description = dish.Description,
Status = dish.Status,
UpdateTime = dish.UpdateTime,
CategoryName = category.Name
};
// 使用Join …… into进行左外连接
var dishListResps = (
from dish in _dbContext.Dishes
join dishFlavor in _dbContext.DishFlavors
on dish.Id equals dishFlavor.DishId into dishFlavorGroup
select new DishListResp
{
Id = dish.Id,
Name = dish.Name,
CategoryId = dish.CategoryId,
Price = dish.Price,
Image = dish.Image,
Description = dish.Description,
Status = dish.Status,
UpdateTime = dish.UpdateTime,
Flavors = dishFlavorGroup.ToList()
}
).ToList();
// 使用 from 子句展平结果,代替SelectMany
var dishPageVos = (
from dish in pagedDishes
// 使用 join...into 进行分组连接
join category in _dbContext.Categories
on dish.CategoryId equals category.Id into categoriesGroup
// 使用 from 子句展平结果
from c in categoriesGroup.DefaultIfEmpty()
// 选择新的 DishVo 对象
select new DishVo
{
Id = dish.Id,
Name = dish.Name,
CategoryId = dish.CategoryId,
Price = dish.Price,
Image = dish.Image,
Description = dish.Description,
Status = dish.Status,
UpdateTime = dish.UpdateTime,
// 如果 category 为 null,则将 CategoryName 设置为 "未分类",否则设置为 category 的 Name
CategoryName = c != null ? c.Name : "未分类"
}
).ToList();
总结
对于数据库关系来说,对一关系使用内连接来完成,高效简单,对于有需求用到外连接的内容,使用from子句展平对象后取值即可
对多关系多用外连接处理,使用into关键字将被链接表做成集合,可调用ToList转至集合后直接赋值到VO对象的属性中
对于LINQ的语法方式选择来说,简单语句使用LINQ方法语法更简单,利用Lambda表达式合适
复杂语法,涉及到多表查询的语法要选择方法语法简单,连接写起来更类SQL
// 三表联查
var dishListResps = from dish in queryable
join dishFlavor in _dbContext.DishFlavors
on dish.Id equals dishFlavor.DishId into dishFlavorGroup
join category in _dbContext.Categories
on dish.CategoryId equals category.Id
select new DishListResp
{
Id = dish.Id,
Name = dish.Name,
CategoryId = dish.CategoryId,
Price = dish.Price,
Image = dish.Image,
Description = dish.Description,
Status = dish.Status,
UpdateTime = dish.UpdateTime,
// 来自于分类表的分类名称
CategoryName = category.Name,
// 来自于口味表中的口味数据
Flavors = dishFlavorGroup.ToList()
};