Search code examples
ruby-on-railsunit-testinghas-and-belongs-to-manyminitestruby-on-rails-5

Getting errors with Minitest when I have a timestamp for a has many through jointable


I have a join table for a has many and belongs to many through, the join table including many other attributes has a timestamp, implementation wise there is no trouble,

User

class User < ApplicationRecord

  has_many :affiliations
  has_many :organizations, through: :affiliations

end

Organization

class Organization < ApplicationRecord

  has_many :affiliations
  has_many :users, through: :affiliations

end

Affiliation

class Affiliation < ApplicationRecord

  belongs_to :user
  belongs_to :organization

  has_many :xxxxxs
end

Affiliation stores not just the belongs, it itself holds information like what the user's rank and what not is in the organization. It is pretty much a strong model of its own.

For fixtures, I do not have a file for the jointable yet,

user.yml

user1:
  email: [email protected]
  organizations: org1

organization.yml

org1
  name: foo

but when I run tests using minitest, it gives me an error.

Error:
PublicControllerTest#test_should_get_index:
ActiveRecord::StatementInvalid: Mysql2::Error: Field 'created_at' doesn't have a default value: INSERT INTO `affiliations` (`user_id`, `dominion_id`) VALUES (794918725, 299359653)

Odd thing is, it occurs on tests that don't even use the said table,

class PublicControllerTest < ActionController::TestCase

  test "should get index" do
    get :index
    assert_response :success
  end
end

This action does absolutely nothing, at this point its just plain html

class PublicController < ApplicationController
  def index
  end
end

does nothing in the controller.

They go away when a remove the timestamps, but recording when the association was created is necessary information. Is there something I need to do in the tests?

I am using Rails edge (5.0.0rc1) is there any chance that this is causing the errors?


Solution

  • Update 3.

    Having "organizations: org1" for your user1 in fixtures seed data - seems this is causing the issue, because user can be connected to organization only through your joint table.

    I didn't find anything explicit in spec, but something relevant here

    Fixtures bypass the normal Active Record object creation process. After reading them from YAML file, they are inserted into database directly using insert query. So they skip callbacks and validations check. This also has an interesting side-effect which can be used for drying up fixtures.

    Update 2.

    I was wrong at assumption that you can't have timestamps in has_and_belongs_to_many jointable managed by Rails. In fact, inside HasAndBelongsToMany Rails will create an ActiveRecord::Base class for that table - here

    def through_model
       habtm = JoinTableResolver.build lhs_model, association_name, options
       join_model = Class.new(ActiveRecord::Base) {
         class << self;
         ...
    

    And ActiveRecord::Base include Timestamp module

    So your error should be caused by some other way of creating an entry in jointable other then standard Rails association.


    Original.

    I don't believe that you can have automatically managed timestamp fields in jointable for has_and_belongs_to_many relation in ActiveRecord. This didn't (intentionally) work in old Rails (e.g. 3.2 - link below), and it don't sound like it changed recently.

    If you want to have extended join table, you may create a dedicated ActiveRecord model for it and use use has_many :through association. This way it will automatically support timestamps should you add it to table definition.

    See https://github.com/rails/rails/issues/4653 for timestamps on HABTM jointable

    AFAICT Rails 3.1 does not populate timestamps on a join table. The only difference is that in 3.2, when you add timestamps, they are marked as NOT NULL.

    @veganstraightedge the timestamps didn't "work" in 3.1 - they just didn't raise an error when the join table was saved with them as null. the difference here is that in 3.2 timestamps are created with a NOT NULL constraint.

    Basically, this can come from an idea that you don't have ActiveRecord model class for the jointable (update 2 - actually you have!), and timestamps are feature of ActiveRecord model. Timestamps in Rails 5.0rc1 hasn't changed a lot - sources - Timestamp is a module that extends ActiveRecord class.

    By the way, it's now suggested to use create_join_table migration helper that will create "pure" table (two id's only, no timestamps): https://github.com/rails/rails/pull/4726

    SO Question with similiar error - Rails 3.2 + MySQL: Error: Field 'created_at' doesn't have a default value: INSERT INTO

    Rails 3.2 doesn't automatically populate the timestamp fields for join tables of :habtm relationships.


    Alternatively (warning - theory!), you can try using either Association callbacks or Association extensions - http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

    Association callbacks

    Similar to the normal callbacks that hook into the life cycle of an Active Record object, you can also define callbacks that get triggered when you add an object to or remove an object from an association collection.

    class Project
      has_and_belongs_to_many :developers, after_add: :evaluate_velocity
    
      def evaluate_velocity(developer)
        ...
      end
    end
    

    Extensions

    The extension argument allows you to pass a block into a has_and_belongs_to_many association. This is useful for adding new finders, > creators and other factory-type methods to be used as part of the association.

    has_and_belongs_to_many :contractors do
      def find_or_create_by_name(name)
        first_name, last_name = name.split(" ", 2)
        find_or_create_by(first_name: first_name, last_name: last_name)
      end
    end
    

    Extensions can refer to the internals of the association proxy using these three attributes of the proxy_association accessor:

    proxy_association.owner returns the object that the association is a part of. 
    proxy_association.reflection returns the reflection object that describes the association. 
    proxy_association.target returns the associated object for belongs_to or has_one, or the collection of associated objects for has_many or has_and_belongs_to_many.