Search code examples
ruby-on-railspostgresqlactiverecordmany-to-many

MTM relationship in Ruby-on-Rails 4 and PostgreSQL using GUIDs as ID's


new to Rails and using Ruby On Rails 4.0.3 with Ruby 2.2.0 with PostgreSQL, I'm attempting to have this database sctructure using GUID's as Id's.

My Goal: I want to have many phone numbers belong to many people as an example.

So i built a little test app which can be found here and basically followed the Association guide to create a Many-to-Many table.

So here is my model with comments added for clarity:

class Address < ActiveRecord::Base
    belongs_to  :contact
end

class Contact < ActiveRecord::Base
    has_many    :addresses
    has_many    :phoneNumbers
end

class PhoneNumber < ActiveRecord::Base
    has_many    :contacts
end

Here is the migration setup:

class CreateAddresses < ActiveRecord::Migration
  def change
    create_table :addresses, id: :uuid do |t|
      #t.belongs_to :contacts
      t.text                :street1
      t.text                :city

      t.timestamps
    end

    add_column  :addresses, :contact_id,    :uuid
  end
end

class CreateContacts < ActiveRecord::Migration
  def change
    create_table :contacts, id: :uuid do |t|
      t.text :firstName
      t.text :lastName

      t.timestamps
    end
  end
end

class CreatePhoneNumbers < ActiveRecord::Migration
  def change
    create_table :phone_numbers, id: :uuid do |t|
      t.text :number
      t.text :name

      t.timestamps
    end
  end
end

# the MTM
class CreateContactsPhoneNumbers < ActiveRecord::Migration
  def change
    create_table    :contacts_phone_numbers, id:  :uuid

    add_column  :contacts_phone_numbers,    :contact_id,    :uuid
    add_column  :contacts_phone_numbers,    :phone_number_id,   :uuid
  end
end

The SQL is output as such:

CREATE TABLE addresses (
    id uuid DEFAULT uuid_generate_v4() NOT NULL,
    street1 text,
    city text,
    created_at timestamp without time zone,
    updated_at timestamp without time zone,
    contact_id uuid
);

CREATE TABLE contacts (
    id uuid DEFAULT uuid_generate_v4() NOT NULL,
    "firstName" text,
    "lastName" text,
    created_at timestamp without time zone,
    updated_at timestamp without time zone
);

CREATE TABLE phone_numbers (
    id uuid DEFAULT uuid_generate_v4() NOT NULL,
    number text,
    name text,
    created_at timestamp without time zone,
    updated_at timestamp without time zone
);

CREATE TABLE contacts_phone_numbers (
    id uuid DEFAULT uuid_generate_v4() NOT NULL,
    contact_id uuid,
    phone_number_id uuid
);

and here is a part of my example seed script

varC = Contact.create(firstName: "John", lastName: "Doe")
varA = Address.create(street1: "10 Somewhere Pl", city: "Ogurek", contact: varC)

Note here that you can succesfully get the contact when querying like so after jsut this migration

puts Address.first.contact.firstname
# John

however, once I chagne the seed script to this:

varC = Contact.create(firstName: "John", lastName: "Doe")
varA = Address.create(street1: "10 Somewhere Pl", city: "Ogurek", contact: varC)

varP = PhoneNumber.create(number: "2342342345", name: "home")
varC.phoneNumbers = varP
varC.save

varP.contacts = varC
varP.save

and I run rake db:migrate, I'll get the following output (note the error):

==  EnablePostgresUuid: migrating =============================================
-- execute("create extension \"uuid-ossp\"")
   -> 0.0018s
==  EnablePostgresUuid: migrated (0.0024s) ====================================

==  CreateAddresses: migrating ================================================
-- create_table(:addresses, {:id=>:uuid})
   -> 0.0036s
-- add_column(:addresses, :contact_id, :uuid)
   -> 0.0005s
==  CreateAddresses: migrated (0.0053s) =======================================

==  CreateContacts: migrating =================================================
-- create_table(:contacts, {:id=>:uuid})
   -> 0.0024s
==  CreateContacts: migrated (0.0031s) ========================================

==  CreatePhoneNumbers: migrating =============================================
-- create_table(:phone_numbers, {:id=>:uuid})
   -> 0.0023s
==  CreatePhoneNumbers: migrated (0.0029s) ====================================

==  CreateContactsPhoneNumbers: migrating =====================================
-- create_table(:contacts_phone_numbers, {:id=>:uuid})
   -> 0.0014s
-- add_column(:contacts_phone_numbers, :contact_id, :uuid)
   -> 0.0004s
-- add_column(:contacts_phone_numbers, :phone_number_id, :uuid)
   -> 0.0003s
==  CreateContactsPhoneNumbers: migrated (0.0038s) ============================

** Invoke db:seed (first_time)
** Execute db:seed
** Invoke db:abort_if_pending_migrations (first_time)
** Invoke environment (first_time)
** Execute environment
** Execute db:abort_if_pending_migrations
rake aborted!
undefined method `each' for #<PhoneNumber:0xa532460>
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/activemodel-4.0.3/lib/active_model/attribute_methods.rb:439:in `method_missing'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/activerecord-4.0.3/lib/active_record/attribute_methods.rb:155:in `method_missing'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/activerecord-4.0.3/lib/active_record/associations/collection_association.rb:333:in `replace'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/activerecord-4.0.3/lib/active_record/associations/collection_association.rb:42:in `writer'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/activerecord-4.0.3/lib/active_record/associations/builder/association.rb:78:in `phoneNumbers='
/_apps/test_site/rails_test1/rtest1/db/seeds.rb:5:in `<top (required)>'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/activesupport-4.0.3/lib/active_support/dependencies.rb:223:in `load'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/activesupport-4.0.3/lib/active_support/dependencies.rb:223:in `block in load'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/activesupport-4.0.3/lib/active_support/dependencies.rb:214:in `load_dependency'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/activesupport-4.0.3/lib/active_support/dependencies.rb:223:in `load'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/railties-4.0.3/lib/rails/engine.rb:540:in `load_seed'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/activerecord-4.0.3/lib/active_record/tasks/database_tasks.rb:154:in `load_seed'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/activerecord-4.0.3/lib/active_record/railties/databases.rake:181:in `block (2 levels) in <top (required)>'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/rake-10.1.1/lib/rake/task.rb:236:in `call'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/rake-10.1.1/lib/rake/task.rb:236:in `block in execute'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/rake-10.1.1/lib/rake/task.rb:231:in `each'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/rake-10.1.1/lib/rake/task.rb:231:in `execute'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/rake-10.1.1/lib/rake/task.rb:175:in `block in invoke_with_call_chain'
/home/vagrant/.rvm/rubies/ruby-head/lib/ruby/2.2.0/monitor.rb:211:in `mon_synchronize'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/rake-10.1.1/lib/rake/task.rb:168:in `invoke_with_call_chain'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/rake-10.1.1/lib/rake/task.rb:161:in `invoke'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/rake-10.1.1/lib/rake/application.rb:149:in `invoke_task'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/rake-10.1.1/lib/rake/application.rb:106:in `block (2 levels) in top_level'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/rake-10.1.1/lib/rake/application.rb:106:in `each'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/rake-10.1.1/lib/rake/application.rb:106:in `block in top_level'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/rake-10.1.1/lib/rake/application.rb:115:in `run_with_threads'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/rake-10.1.1/lib/rake/application.rb:100:in `top_level'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/rake-10.1.1/lib/rake/application.rb:78:in `block in run'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/rake-10.1.1/lib/rake/application.rb:165:in `standard_exception_handling'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/rake-10.1.1/lib/rake/application.rb:75:in `run'
/home/vagrant/.rvm/gems/ruby-head@rails4/gems/rake-10.1.1/bin/rake:33:in `<top (required)>'
/home/vagrant/.rvm/gems/ruby-head@rails4/bin/rake:23:in `load'
/home/vagrant/.rvm/gems/ruby-head@rails4/bin/rake:23:in `<main>'
Tasks: TOP => db:seed

Thus my Question: How can I add a queryable model(s) that can have a MTM relationship using ActiveRecord and how do I save and query it?


Solution

  • Ok, so I finally figured it out, here it is in all of its glory:

    First, I had to change to using a has_and_belongs_to_many association instead of a has_many :through like I was trying to (although I'm sure I could get the has_many :through figured out somehow as well), yet as I understand the guide, since I don't care about the join table, I can just use the has_and_belongs_to_many association. Yet, our problem is that we're using GUID's and not Integers.

    So I missed something in the Associations guid, there is one line and its in parentheses:

    (though you'll need to remember to create the joining table in the database)

    So I changed my model to this:

    class Contact < ActiveRecord::Base
        has_many            :addresses
        has_and_belongs_to_many :phoneNumbers
    end
    
    class PhoneNumber < ActiveRecord::Base
    has_and_belongs_to_many :contacts
    end
    

    and migration:

    class CreateContacts < ActiveRecord::Migration
      def change
        create_table :contacts, id: :uuid do |t|
          t.text :firstName
          t.text :lastName
    
          t.timestamps
        end
      end
    end
    
    class CreatePhoneNumbers < ActiveRecord::Migration
      def change
        create_table :phone_numbers, id: :uuid do |t|
          t.text :number
          t.text :name
    
          t.timestamps
        end
      end
    end
    

    I then added this migration:

    class CreateContactsPhoneNumbers < ActiveRecord::Migration
      def change
        create_table    :contacts_phone_numbers, id:  :uuid do |t|
          t.uuid :contact_id
          t.uuid :phone_number_id
        end
      end
    end
    

    all of the above churned out the following SQL:

    CREATE TABLE contacts (
        id uuid DEFAULT uuid_generate_v4() NOT NULL,
        "firstName" text,
        "lastName" text,
        created_at timestamp without time zone,
        updated_at timestamp without time zone
    );
    
    CREATE TABLE phone_numbers (
        id uuid DEFAULT uuid_generate_v4() NOT NULL,
        number text,
        name text,
        created_at timestamp without time zone,
        updated_at timestamp without time zone
    );
    
    CREATE TABLE contacts_phone_numbers (
        id uuid DEFAULT uuid_generate_v4() NOT NULL,
        contact_id uuid,
        phone_number_id uuid
    );
    

    and I can add models like such taken from my seeds.rb:

    c = Contact.create(firstName: "John", lastName: "Smith")
    p = PhoneNumber.create(number: "2342342345", name: "home", contacts: [c])
    

    I put the final solution up on the BitBucket test project, please see Commit 762a546.

    So, if anyone has any pointers to how I can fix this to be more in line with how the real Rubyists do it, I would love to here it in the comments. Thanks.