如果我有一个连接两个表的连接模型,我如何在一个查询中获取所有需要的记录?

If I have a join model that joins two tables, how do I fetch all of the needed records in one query?

我有三张表食谱、配料和 recipe_ingredients(加入前两个)

在我的 recipe_controller 中,我想要我的索引方法 return 所有需要的数据来向用户显示食谱。

架构看起来像(删除了无用的信息):

"ingredients",
    t.string "name"
    t.string "food_group"
  end

"recipe_ingredients",
    t.bigint "recipe_id"
    t.bigint "ingredient_id"
    t.integer "quantity"
    t.string "measurement_unit"
  end

"recipes",
    t.string "name"
    t.string "genre"
    t.bigint "user_id"

我试过

  def index
    recipe = Recipe.last
    recipe_ingredients = RecipeIngredient.where(recipe_id: recipe.id)
    ingredient_ids = recipe_ingredients.pluck(:id)
    ingredients = Ingredient.where('id in (?)', ingredient_ids)
    render json: { 
                  recipe: recipe, 
                  recipe_ingredients: recipe_ingredients,
                  ingredients: ingredients }
  end

至少让一条记录正确,但我不确定在尝试创建通用索引方法而不编写讨厌的低效循环时从哪里开始。

此外,我是 rails sql 的新手,这很棒,但我确定我在这里遗漏了很多优化,所以如果有什么我可以做的改进以上请告诉我。

我认为问题出在你的配料上。

首先你可以得到这样的recipe_ingredients

recipe_ingredients = recipe.recipe_ingredients

另一件事是你没有得到成分的 id,但这些是 recipe_ingredients,这是桥梁 table。

ingredient_ids = recipe_ingredients.pluck(:ingredient_id)

现在你的代码应该是这样的

def index
  recipe = Recipe.last
  recipe_ingredients = recipe.recipe_ingrediants
  ingredient_ids = recipe_ingredients.pluck(:ingredient_id)
  ingredients = Ingredient.where(id: ingredient_ids)
  render json: { 
              recipe: recipe, 
              recipe_ingredients: recipe_ingredients,
              ingredients: ingredients }

结束

您可以在您的收件人模型中创建以下关联

has_many :recipe_ingredients
has_many :ingredients, through: :recipe_ingredients

在你的控制器中

def index
  @recipies = Recipie.includes(recipe_ingredients: :ingredients)
end

这将在单个查询中加载所有食谱、recipe_ingredients 和成分。在您看来,您可以迭代 @recipies

@recipies.each do |recipie|
  recipe_ingredients = recipie.recipe_ingredients
  ingredients = recipie.ingredients
end

更新:

def index
  @recipies = Recipie.includes(recipe_ingredients: :ingredients)
  recipie_with_ingredients = @recipies.map do |recipie|
                               {
                                 recipie: recipie,
                                 recipe_ingredients: recipie.recipe_ingredients,
                                 ingredients: recipie.ingredients
                               }
                             end
render json: recipie_with_ingredients
end

我假设您实际上并不需要 JSON 结构。仅仅是因为考虑到总体目标,它实际上没有意义。如果您想呈现一个食谱,您实际上希望将数量、名称和单位放在一起,这样您就不会在消费端从两个单独的数组中拼凑所需的信息。

理想情况下,您希望 JSON 类似于:

{
  "name": "Chocolate Chip Cookies",
  "ingredients": [
    {
      "name": "Butter",
      "quantity": 100,
      "measurement_unit": "gr"
    },
    {
      "name": "Flour",
      "quantity": 300,
      "measurement_unit": "gr"
    }
  ]
}

前提是您有以下协会:

class Recipe < ApplicationRecord
  has_many :recipe_ingredients
end
class RecipeIngredient < ApplicationRecord
  belongs_to :recipe
  belongs_to :ingredient
end

您可以在单个查询中加载所有三个表的记录:

@recipe = Recipe.eager_load(recipe_ingredients: :ingredient).last

这会生成以下怪物查询:

SELECT
  "recipes"."id" AS t0_r0,
  "recipes"."name" AS t0_r1,
  "recipes"."created_at" AS t0_r2,
  "recipes"."updated_at" AS t0_r3,
  "recipe_ingredients"."id" AS t1_r0,
  "recipe_ingredients"."recipe_id" AS t1_r1,
  "recipe_ingredients"."ingredient_id" AS t1_r2,
  "recipe_ingredients"."quantity" AS t1_r3,
  "recipe_ingredients"."measurement_unit" AS t1_r4,
  "recipe_ingredients"."created_at" AS t1_r5,
  "recipe_ingredients"."updated_at" AS t1_r6,
  "ingredients"."id" AS t2_r0,
  "ingredients"."name" AS t2_r1,
  "ingredients"."created_at" AS t2_r2,
  "ingredients"."updated_at" AS t2_r3 
FROM
  "recipes" 
  LEFT OUTER JOIN
    "recipe_ingredients" 
    ON "recipe_ingredients"."recipe_id" = "recipes"."id" 
  LEFT OUTER JOIN
    "ingredients" 
    ON "ingredients"."id" = "recipe_ingredients"."ingredient_id"  
LIMIT 1

请注意列别名 - 这就是 ActiveRecord 知道将结果集中的每一列填充到哪个模型的方式。

这将使您无需 N+1 查询就可以遍历关联 - 就像这个 HTML 示例:

<article class="recipe">
  <h1><%= @recipe.name %></h1>
  <ul class="ingredients">
  <%= @recipe.recipe_ingredients.each do |ri| %>
    <li>
      <%= ri.quantity %><%= ri.measurement_unit %> <%= ri.ingredient.name %>
    </li>
  <% end %>
  </ul>
</article>

像其他答案一样设置间接关联通常是个好主意,但是当您真正想做的是遍历 [=] 时,.eager_load(:ingredients) 实际上不会阻止 n+1 查询18=] 关联和嵌套成分。

虽然您可以将其呈现为 JSON 并使用:

render json: @recipe, 
       include: { recipe_ingredients: :ingredient }

将任何繁重的工作转移到单独的序列化层中通常是一个更好的主意,例如 ActiveModel::Serializers 或许多 JSONAPI gems 之一。