We've moved discussions to Discord

[SOLVED] belongs_to is nil for channel attribute on ChannelUser instance

Jake Smith
Hey Chris Oliver , I wasn't sure if this was the right place to mention this, but I am putting the Group Chat based on your GoRails lessons into my jumpstart-based app and I'm running into this issue: https://stackoverflow.com/questions/64765493/rails-belongs-to-returning-nil-even-though-foreign-key-is-valid

Basically, I implemented an instance method on the ChannelUser class to get the unread messages for that user and that channel.  This is the method implementation:

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

I'm attempting to put a total count of unread messages from all joined channels on the user's dashboard page when signed into one of their non-personal accounts:

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

Here is the error I'm getting:
image.png 94.5 KB

I'm just not sure what could be going wrong here.  The channel attribute returns nil, but channel_id is a valid id of a Channel record, and finding it myself works in the web-console.  But even MORE strange is that if I change channel.messages.('created_at > ?', last_read_at) to Channel.find(channel_id).('created_at > ?', last_read_at), I still get the same error!

Any ideas?
Jake Smith
I took a slightly different approach to see if I could get different results, but I'm still just as baffled, if not more:
image.png 83.9 KB
Jake Smith
In case it wasn't obvious, in my second message, I implemented the `unread_messages` method in the `User` model instead to see if that would make a difference.  However, I see more weirdness.  As in, the same code that errors when running, seems to not error at all any more when I copy and paste the same thing in the console, supposedly with similar scope to when the error was throne.
Jake Smith
This seemed like such a solvable problem, but I'm still running into this issue.  The stackoverflow post isn't getting much attention, but there is some more detail there in case anyone feels like looking into this with me.
Jake Smith
Chris Oliver I'm still really confused by this issue, but I think it has something to do with the multitenancy of the jumpstart application template.

Here is my current implementation of the unread_messages method:

image.png 22 KB

This is in the User class.  When I'm debugging this, I put a breakpoint inside flat_map so that I could look into each iteration.  This is where I learned something multitenancy-related was going on:

image.png 37.6 KB

From what I can tell, cu.channel is returning nil because, and this is the part I don't understand, within this context, Channel.find(4) isn't generating a sql query that looks for channels where id = 4, but instead looking for channels where account_id = 4.  I'm not sure what is causing the find method to behave differently than what I thought it was supposed to do.  But when I go to the rails console, Channel.find(4) behaves as expected:
image.png 26.6 KB

Since this seems to be related to how multitenancy is implemented in this app, I was wondering if you could help me diagnose why this is behaving this way?
Jake Smith
OMG I finally figured it out...

So it was definitely related to multi-tenancy.  Once I realized and remembered that there was middleware filtering resources by account_id if the resource type had an account_id attribute, this explained why getting the error while running the app was not happening when running the same queries in the console.

Then the reason came to me: Once I realized that chat Channels don't really make sense for a personal account, I had already created several chat Channels in my development database during initial implementation.  So there were Channels floating around in the database that were associated to my User through ChannelUsers, but the account_id on them did not match the current_account.id.  Thus, nil was an appropriate return value.

Once I cleaned up the database by removing any Channels that were associated with a personal account, I stopped getting the error.  Then I wondered if the unread_messages method should be a little less brittle to potentially handle this better in the future.  I could put safe navigation operators inside the block grabbing the unread messages for each channel. But then I thought that I really do only want Channels to be associated with non-personal accounts, so I'd much rather know when the data is in a state where my assumptions don't hold true.  Thus, I thought it was better to add more validation to the Channel itself upon creation:

class Channel < ApplicationRecord
  ...
  validates :name, presence: true
  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
Jake Smith
I decided that this will likely be something I need in the future, so I made it a concern:

module OnlyNonPersonalAccounts
  extend ActiveSupport::Concern

  included do
    validate :only_non_personal_account
  end

  private

  def only_non_personal_account
    return unless account&.personal?

    errors.add(:account, "#{self.class.name.pluralize} are only supported for non-personal accounts")
  end
end

Chris Oliver would this be useful (or something like it) in the standard offering for Jumpstart?

I also spent WAY too long trying to come up with a good name for the concern.  Initially, I got stuck trying to force the name to end in "-able."  Can you think of a better name for this concern than OnlyNonPersonalAccountsOnlyNonPersonalAccountAccessible?

Notifications
You’re not receiving notifications from this thread.