Rails belongs_to 即使外键有效也返回 nil

Rails belongs_to returning nil even though foreign key is valid

总结

我 运行 遇到一个问题,我的 belongs_to 关联 return 正在 nil 而我认为不应该。

我想做什么

A ChannelUser 应该能够 return 来自关联 channel 的消息,其中 created_at 大于上次它的 user 最后一次阅读此 channel。 class 定义如下:

class ChannelUser < ApplicationRecord
  belongs_to :channel
  belongs_to :user

  scope :with_channel, -> { includes(:channel) }

  def unread_messages
    channel.messages.where('created_at > ?', last_read_at)
  end
end

class User < ApplicationRecord
  has_many :channel_users, -> { includes(:channel) }, dependent: :destroy
  has_many :channels, through: :channel_users
  has_many :messages
end

class Channel < ApplicationRecord
  has_many :channel_users, dependent: :destroy
  has_many :users, through: :channel_users
  has_many :messages, -> { order(created_at: :asc).limit(100) }, dependent: :destroy
end

class Message < ApplicationRecord
  belongs_to :channel
  belongs_to :user
  has_rich_text :body
  ...
end

我正在尝试呈现来自用户已加入视图的所有频道的未读消息总数:

<%= current_user.channel_users.sum(&:unread_messages) %>

Userclass有has_many :channel_usershas_many :channels, through: :channel_usersChannel class 有 has_many :channel_usershas_many :users, through: :channel_users.

问题

当我在 rails 控制台中使用任何 ChannelUser 实例执行 unread_messages 方法时,我没有收到错误。但是,当我 运行 开发中的应用程序时,我得到:

正如您在图片底部的网络控制台中看到的,channel returns nil 尽管 channel_id 是一个有效的外键。事实上,架构是这样定义的:

class CreateChannelUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :channel_users do |t|
      t.belongs_to :channel, null: false, foreign_key: true
      t.belongs_to :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end

我还在带有 <% debugger %> 的视图中放置了一个断点,并查看了 current_usercurrent_user.channel_userscurrent_user.channel_users.first.unread_messages 的值。对于 current_user,我得到了与我的登录帐户关联的 User 实例。对于 current_user.channel_users,我得到了代表我加入的聊天频道的记录。对于 curretn_user.channel_users.first.unread_messages,我没有收到错误,我得到一个空的关系集,正确识别我没有任何未读消息。重要的是要注意,当我不汇总所有未读消息的总和时,不会发生此错误。

更新

我发现我可以通过单击调用堆栈中的行来更改 Web 控制台的范围,因此这里是 Web 控制台的更多输出:

>> current_user.channel_users

=> #<ActiveRecord::Associations::CollectionProxy [#<ChannelUser id: 7, channel_id: 5, user_id: 1, created_at: "2020-11-09 06:42:52", updated_at: "2020-11-15 23:44:28", last_read_at: "2020-11-15 23:44:28">, #<ChannelUser id: 3, channel_id: 4, user_id: 1, created_at: "2020-11-09 06:26:12", updated_at: "2020-11-09 06:27:03", last_read_at: "2020-11-09 06:27:03">, #<ChannelUser id: 4, channel_id: 6, user_id: 1, created_at: "2020-11-09 06:30:41", updated_at: "2020-11-15 23:32:26", last_read_at: "2020-11-15 23:32:26">, #<ChannelUser id: 8, channel_id: 7, user_id: 1, created_at: "2020-11-10 01:20:44", updated_at: "2020-11-15 23:33:24", last_read_at: "2020-11-15 23:33:24">]>

>> current_user.channel_users.map(&:unread_messages)

NoMethodError: undefined method `messages' for nil:NilClass
    from /home/jakehockey10/Code/team-portal/app/models/channel_user.rb:29:in `unread_messages'
    from /home/jakehockey10/Code/team-portal/app/views/dashboard/show.html.erb:9:in `map'
    from /home/jakehockey10/Code/team-portal/app/views/dashboard/show.html.erb:9:in `_app_views_dashboard_show_html_erb___259171981150348787_116880'

>> current_user.channel_users.first.unread_messages

NoMethodError: undefined method `messages' for nil:NilClass
    from /home/jakehockey10/Code/team-portal/app/models/channel_user.rb:29:in `unread_messages'
    from /home/jakehockey10/Code/team-portal/app/views/dashboard/show.html.erb:9:in `_app_views_dashboard_show_html_erb___259171981150348787_116880'

>> current_user.channel_users.first

=> #<ChannelUser id: 7, channel_id: 5, user_id: 1, created_at: "2020-11-09 06:42:52", updated_at: "2020-11-15 23:44:28", last_read_at: "2020-11-15 23:44:28">

>> current_user.channel_users.first.channel

=> nil

>> Channel.find(5)

=> #<Channel id: 5, name: "general", account_id: 2, created_at: "2020-11-09 06:30:32", updated_at: "2020-11-09 06:30:32", direct_message: false>

最后一个真的让我感到困惑。通过 current_user 的 channel_users 关联请求频道,我得到 nil,但是当我看到 channel_id 是 5 时,我请求带有 id 的频道5到Channel,我能找到就好了。给了什么?

我试过的另一件事

我在 rails 控制台中继续查询我正在查询的相同内容,并且没有出现错误:

> User.first.channel_users.sum(&:unread_messages)
  User Load (0.4ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT   [["LIMIT", 1]]
  ChannelUser Load (0.2ms)  SELECT "channel_users".* FROM "channel_users" WHERE "channel_users"."user_id" =   [["user_id", 1]]
  Channel Load (0.3ms)  SELECT "channels".* FROM "channels" WHERE "channels"."id" IN (, , , )  [["id", 5], ["id", 4], ["id", 6], ["id", 7]]
  Message Load (0.4ms)  SELECT "messages".* FROM "messages" WHERE "messages"."channel_id" =  AND (created_at > '2020-11-15 23:44:28.787257') ORDER BY "messages"."created_at" ASC LIMIT   [["channel_id", 5], ["LIMIT", 100]]
  Message Load (0.2ms)  SELECT "messages".* FROM "messages" WHERE "messages"."channel_id" =  AND (created_at > '2020-11-09 06:27:03.105358') ORDER BY "messages"."created_at" ASC LIMIT   [["channel_id", 4], ["LIMIT", 100]]
  Message Load (0.2ms)  SELECT "messages".* FROM "messages" WHERE "messages"."channel_id" =  AND (created_at > '2020-11-15 23:32:26.070403') ORDER BY "messages"."created_at" ASC LIMIT   [["channel_id", 6], ["LIMIT", 100]]
  Message Load (0.2ms)  SELECT "messages".* FROM "messages" WHERE "messages"."channel_id" =  AND (created_at > '2020-11-15 23:33:24.022821') ORDER BY "messages"."created_at" ASC LIMIT   [["channel_id", 7], ["LIMIT", 100]]

=> []

另一个更新

这是锦上添花:

我不知道这是怎么回事,我想知道这是否是某种名称冲突问题或其他问题。我该如何诊断?

更新

同样的问题,尝试不同的方法:

为什么它会在这一行抛出错误,但如果我将同一行粘贴到错误页面的网络控制台中,却不会抛出错误?我错过了什么?

尝试删除 includes,我不是 100% 确定,但这可能是潜在的问题。

class User < ApplicationRecord
  has_many :channel_users, dependent: :destroy

您可以将模型中的 includes 替换为 <%= current_user.channel_users.includes(:channel).sum(&:unread_messages) %>

我 95% 确定问题来自消息模型!

好像channel.messages不等于channel.user.messages。

首先,你没有向我们展示你问题中的消息模型,请更新问题。

其次,您有两个模型:渠道和用户,它们都与消息模型[=45]有关联=] (has_many :messages),对我来说这不是很好的关联(在看到消息模型之前我无法考虑解决方案)。

第三,在您的查询中:

User.first.channel_users.sum(&:unread_messages)

其中一行:

Message Load (0.4ms)  SELECT "messages".* FROM "messages" WHERE "messages"."channel_id" =  AND (created_at > '2020-11-15 23:44:28.787257') ORDER BY "messages"."created_at" ASC LIMIT   [["channel_id", 5], ["LIMIT", 100]]
  Message Load (0.2ms)  SELECT "messages".* FROM "messages" WHERE "messages"."channel_id" =  AND (created_at > '2020-11-09 06:27:03.105358') ORDER BY "messages"."created_at" ASC LIMIT   [["channel_id", 4], ["LIMIT", 100]]
  Message Load (0.2ms)  SELECT "messages".* FROM "messages" WHERE "messages"."channel_id" =  AND (created_at > '2020-11-15 23:32:26.070403') ORDER BY "messages"."created_at" ASC LIMIT   [["channel_id", 6], ["LIMIT", 100]]
  Message Load (0.2ms)  SELECT "messages".* FROM "messages" WHERE "messages"."channel_id" =  AND (created_at > '2020-11-15 23:33:24.022821') ORDER BY "messages"."created_at" ASC LIMIT   [["channel_id", 7], ["LIMIT", 100]]

例如,第一行是这样的:

Message Load (0.4ms)  SELECT "messages".* FROM "messages" WHERE "messages"."channel_id" = 5 AND (created_at > '2020-11-15 23:44:28.787257') ORDER BY "messages"."created_at" ASC LIMIT 100

这意味着:得到 100 条 channel_id = 5 的排序消息。这返回了 null !或 空消息集.

所以问题,对我来说,就是从那里来的。

我终于弄明白了。在此应用程序中,为了提供多租户,每个用户都有一个关联的帐户。帐户可以是个人的或非个人的。登录后,您可以将视角从您的个人帐户更改为与您关联的任何非个人帐户。然后,将具有 account_id 列的资源过滤为当前帐户。这种过滤发生在中间件中,因此与执行相关实际查询的代码分开。

当我第一次开始实施此功能时,我在我的个人帐户上创建了一些频道,然后才尝试将频道作为一个整体功能限制在非个人帐户上下文中。所以我的数据库中有一些频道与个人帐户相关联。

在我的屏幕截图中,该应用程序使用该中间件将频道限制为具有特定当前 account_id 的频道。但是,当我在网络控制台或 rails 控制台中模仿这些相同的查询时,显然没有中间件 运行 过滤器来过滤当前帐户,因为在该上下文中没有当前帐户。这解释了为什么当 运行 应用程序与 运行 控制台中的这些查询时,我总是看到不同的结果。

现在我已经销毁了与个人帐户关联的所有频道,错误消失了。我还添加了一些验证以防止将来出现这种情况:

class Channel < ApplicationRecord

  ...

  validate :account_not_personal

  private

  ...

  def account_not_personal
    errors.add(:account, 'Channels are only supported for non-personal accounts') if account&.personal?
  end
end
require 'test_helper'

class ChannelTest < ActiveSupport::TestCase
  test 'name is required' do
    channel = Channel.new(name: nil)
    assert_not channel.valid?
    assert_not_empty channel.errors[:name]
  end

  test 'account is required' do
    channel = Channel.new(account: nil)
    assert_not channel.valid?
    assert_not_empty channel.errors[:account]
  end

  test 'account should be non-personal' do
    channel = Channel.new(name: 'test', account: Account.new(personal: false))
    assert channel.valid?
  end

  test 'account cannot be personal' do
    channel = Channel.new(name: 'test', account: Account.new(personal: true))
    assert_not channel.valid?
    assert_not_empty channel.errors[:account]
  end
end