发布于 2026-01-06 2 阅读
0

观察方式:ORMS 和 SQL 视图

观察方式:ORMS 和 SQL 视图

本文假设读者对数据库、Ruby 和 Rails 的基本工作原理有所了解——以下示例使用ActiveRecord库将数据库关系映射到 Rails 应用中的 Ruby 对象。您可以查看示例仓库以了解其实际应用!(注:如果您感兴趣,我还在此处提供了一个 JavaScript 示例,但我尚未找到对 SQL 视图提供一流支持的 JS ORM。)

聚集加剧

Web 应用程序通常会显示“摘要”或聚合数据,这些数据可能来自多个数据库表并进行跨表计算。ORM(对象关系映射)数据库建模库通常设计为一次读取一个表。虽然这种模式在许多用例中都很有用(基本的 CRUD 操作非常便捷),但当我们尝试聚合数据时,会面临应用程序代码复杂和/或数据库查询成本高昂的风险。SQL 视图可以将数据聚合逻辑下推到数据库,从而帮助我们减少查询次数。

例如,假设你受雇开发一款帮助兽医与客户沟通的应用程序。如果你创建过服务器端点,那么像这样的(简化版)工作流程示例你可能并不陌生:

user = User.includes(:pets).find(1)

render json: {
  user: {
    id: user.id,
    name: "#{user.first_name} #{user.last_name}"
  },
  pets: user.pets.map do |pet|
    {
      id: pet.id,
      name: pet.name
    }
  end
}
Enter fullscreen mode Exit fullscreen mode

蓝色背景上的坐姿狗

上面的代码非常简单:它加载用户信息,加载用户的宠物信息,并将数据序列化为前端所需的格式。我们执行了两个查询,但它们都很简单,而且(希望)速度很快——这是合理的预期。

几周后,我们又有了新的需求。用户不仅可以看到他们已注册的宠物信息,还希望能够“一目了然”地知道下次预约时间!因此,您需要更新接口:

user = User
  .includes(:pets)
  .find(1)

# Pre-load the user's pets' appointments that are scheduled for the future
upcoming_appointments = user
  .appointments
  .order(date: :asc)  
  .where('date >= ?', Time.current)

render json: {
  user: {
    id: user.id,
    name: "#{user.first_name} #{user.last_name}"
  },
  pets: user.pets.map do |pet|
    # Use Array#find to return the first of this pet's appointments
    next_appointment_date = upcoming_appointments.find do |appt|
      appt.pet_id == pet.id
    end

    {
      id: pet.id,
      name: pet.name,
      next_appointment_date: next_appointment_date
    }
  end
}
Enter fullscreen mode Exit fullscreen mode

说实话,这还算可以。我们添加了另一个查询——这次是针对预约的——并加入了一些排序和筛选逻辑。我们还在序列化步骤中添加了一些循环逻辑,以便提取出每位宠物主人的第一个预约。虽然有点粗糙,但至少能用。

就我个人而言,我不喜欢把所有这些行为都编码到应用程序代码中。我觉得这样很混乱,而且很容易被恶意程序偷偷植入。例如,假设我们这样做:

render json: {
  user: {
    id: user.id,
    name: "#{user.first_name} #{user.last_name}"
  },
  pets: user.pets.map do |pet|
    next_appointment_date = user
      .appointments
      .order(date: :asc)
      .find_by('date >= ?', Time.current)
      &.date

    {
      id: pet.id,
      name: pet.name,
      next_appointment_date: next_appointment_date
    }
  end
}
Enter fullscreen mode Exit fullscreen mode

🙀糟糕!有人偷偷地在我们的序列化步骤中添加了一个查询,虽然一开始可能不会引起任何警觉(当每个用户只有一两只宠物时),但如果当地的动物收容所成为你的客户,并且注册了一百多只宠物呢?每次页面加载都会产生一百多个查询。

这显然是个精心设计的例子,但我确实遇到过更复杂的情况,其中 N+1 查询被隐藏在服务对象和单独的文件中。我个人喜欢把这类逻辑下推到数据库层,尤其是在逻辑变得更加复杂或者需要在应用程序的其他地方使用时。这就是 SQL 视图的用武之地!

什么SQL视图?

我对 SQL 视图最基本的理解是“数据库中保存的查询语句,它就像一个表”。当然,SQL 视图远不止这些 但这种简单的理解足以让你受益匪浅。例如,如果我在数据库中执行以下语句:

CREATE VIEW silly_users AS
  SELECT 
    id, 
    first_name,
    first_name || ' Mc' || first_name || 'erson' AS silly_name
  FROM users;
Enter fullscreen mode Exit fullscreen mode

关于||连接运算符的更多信息

我可以使用与查询表格相同的语法来查询此视图中的结果:

# SELECT * FROM silly_users;
  id   | first_name |       silly_name       
-------+------------+------------------------
     1 | Melissa    | Melissa McMelissaerson
     2 | Colleen    | Colleen McColleenerson
     3 | Vince      | Vince McVinceerson
     4 | David      | David McDaviderson
     5 | Dennis     | Dennis McDenniserson
...etc
Enter fullscreen mode Exit fullscreen mode

由于此视图的行为与表格非常相似,因此可以很好地与 ORM 配合使用。我们可以创建一个视图支持的模型!

silly_user = SillyUser.find(1)
silly_user.silly_name // => 'Melissa McMelissaerson'
Enter fullscreen mode Exit fullscreen mode

如果你正在使用 Ruby,我强烈推荐ThoughtBot 的 Scenic gem,它为使用 ActiveRecord ORM 的项目带来了视图版本控制和其他有用的功能。

新的视角

让我们尝试编写一个数据库视图来获取所需数据,而不是在应用程序代码中进行查询(更多关于如何使用DISTINCT ON数据库视图的信息GROUP BY请参见此处):

-- Grabs one record per pet, returning the earliest future appointment date 
CREATE VIEW pets_with_upcoming_appointments AS
SELECT DISTINCT ON (pets.id)
  pets.id AS id,
  users.id AS user_id,
  pets.name AS name,
  MIN(appointments.date) AS next_appointment_date
FROM users
INNER JOIN pets
  ON user_id = users.id
LEFT JOIN appointments
  ON pets.id = pet_id
  AND appointments.date >= CURRENT_DATE
GROUP BY (
  users.id,
  pets.id,
  pets.name
);

Enter fullscreen mode Exit fullscreen mode

太好了!现在我们可以从这个角度阅读了:

# SELECT * FROM pets_with_upcoming_appointments;

 user_id | pet_id | pet_name |  next_appointment_date   
---------+--------+----------+-----------------------
       1 |      1 | Flannery |   2018-11-22 13:00:00
       2 |      2 | Porkchop |   2018-11-22 16:30:00
       2 |      3 | Gravy    |   2018-12-01 09:00:00
       3 |      4 | Magnus   | 
       4 |      5 | Huey     |   2018-12-15 10:45:00
       4 |      6 | Duey     |   2018-12-15 10:45:00
       4 |      7 | Louie    |   2018-12-15 10:45:00

# SELECT * FROM pets_with_upcoming_appointments WHERE user_id = 1;

 user_id | pet_id | pet_name |  next_appointment_date   
---------+--------+----------+-----------------------
       1 |      1 | Flannery |   2018-11-22 13:00:00
Enter fullscreen mode Exit fullscreen mode

现在视图已经设置好了,我们可以使用前面提到的 Scenic gem 创建一个迁移,然后将其连接到数据库支持的 ORM 模型:

class PetWithUpcomingAppointment < ActiveRecord::Base
  self.table_name = 'pets_with_upcoming_appointments'
end
Enter fullscreen mode Exit fullscreen mode

由于我们的视图有一个user_id字段,因此很容易在我们的 User 模型中建立关联:

class User < ActiveRecord::Base
  has_many :pets
  # wooooot
  has_many :pet_with_upcoming_appointments
  has_many :appointments, through: :pets
end
Enter fullscreen mode Exit fullscreen mode

现在,我们可以清理数据获取应用程序代码了:

user = User
  .includes(:pet_with_upcoming_appointments)
  .find(params[:id])

render json: {
  user: {
    id: user.id,
    name: "#{user.first_name} #{user.last_name}"
  },
  pets: user.pet_with_upcoming_appointments.map do |pet|
    {
      id: pet.id,
      name: pet.name,
      next_appointment_date: pet.next_appointment_date
    }
  end
}
Enter fullscreen mode Exit fullscreen mode

不错!我们现在只需要两个查询,而且控制器里也不需要排序/日期比较逻辑了💪 更棒的是,我们可以在应用程序的其他部分复用这个模型,而无需重复编写查询逻辑。当你开始执行更复杂的聚合操作,并从不同的表中提取数据时,它的优势就更加明显了。

SQL视图的陷阱

猫从洞里探出头来

虽然我显然是它的粉丝,但在考虑应用程序中的视图时,有一些事项需要注意:

过度热衷于浏览

这就是“我手里只有锤子,什么都是钉子!”问题的另一个方面。当你刚开始接触视图时,你可能会发现自己把太多逻辑都推了数据库里(数据库里逻辑被抽象化了,更难找到)。最终,你不得不运行大量的迁移,因为每次应用程序需要更改其提供的内容时,都需要更新或创建一个新的视图。😬 在将你正在处理的内容转换为视图之前,问问自己,你获得的好处是否值得付出这样的代价。

层级过多的层叠视图

视图通常由表的结果构成,但你也可以创建一个视图,从另一个视图中提取数据(元视图?!)。这看起来似乎是个好主意(“我会在这个视图中定义‘活跃’用户,然后在任何需要用户的地方都使用它!”),但实际上,我发现它会让各个层级的视图更新变得更加困难。你还会因为试图弄清楚一个视图的更改如何影响另一个从多个层级提取数据的视图而感到抓狂😵。这个问题只有在与第一点同时出现时才会真正显现出来。

效率低下蔓延

如果您熟悉 SQL 查询分析,并且知道如何使用EXPLAIN`/`EXPLAIN ANALYZE来查找问题(或者如果LEFT JOIN appointments您看到上面的视图时感到不自在 😂),那么现在正是运用这些技能的好时机!如果您对索引哈希连接顺序扫描感到头晕目眩,那么一旦数据库中的记录越来越多,您可能会发现看似简单的查询运行速度非常慢。如果不加注意,低效的查询最终可能会支撑应用程序的大量功能,从而导致整体性能下降。至少,您应该检查一下引入视图对使用该视图的进程速度的影响。

接下来怎么办?

如果您感兴趣,以下是我推荐的一些资源(以及我在本文中提到的链接),供您了解更多信息!祝您长寿、繁荣,并且无惧数据库🖖

文章来源:https://dev.to/annarankin/ways-of-seeing-orms--sql-views-562o