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) %>
User
class有has_many :channel_users
和has_many :channels, through: :channel_users
。 Channel
class 有 has_many :channel_users
和 has_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_user
、current_user.channel_users
和 current_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
总结
我 运行 遇到一个问题,我的 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) %>
User
class有has_many :channel_users
和has_many :channels, through: :channel_users
。 Channel
class 有 has_many :channel_users
和 has_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_user
、current_user.channel_users
和 current_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