views:

512

answers:

2

I know I can use :finder_sql to manually define the SQL to use to fetch associated records, but I'm wondering if ActiveRecord uses the :primary_key and :foreign_key options on an association to generate the joining SQL. It doesn't appear to, but am I just missing something here?


Update: To be more explicit, my question is: Is there some way to keep the id columns as the primary key but have AR use a different column for the joins when I specify my relationships?


Here's example model definitions and an example console session...

Models

class Post < ActiveRecord::Base
  #Columns are: id:integer uuid:string name:string
  has_many :assignments, :foreign_key => 'post_uuid', :primary_key => 'uuid'
  has_many :categories, :through => :assignments
end

class Category < ActiveRecord::Base
  #Columns are id:integer uuid:string name:string
  has_many :assignments, :foreign_key => 'category_uuid', :primary_key => 'uuid'
end

class Assignment < ActiveRecord::Base
  #Columns are post_uuid:string category_uuid:string
  belongs_to :post, :foreign_key => 'uuid', :primary_key => 'post_uuid'
  belongs_to :category, :foreign_key => 'uuid', :primary_key => 'category_uuid'
end

Console Session

#Make a Post
>> p = Post.create(:uuid => '123', :name => 'The Post')
  Post Create (0.9ms)   INSERT INTO "posts" ("name", "created_at", "uuid", "updated_at") VALUES('The Post', '2010-02-04 00:05:13', '123', '2010-02-04 00:05:13')
=> #<Post id: 2, uuid: "123", name: "The Post", created_at: "2010-02-04 00:05:13", updated_at: "2010-02-04 00:05:13">

#Make a Category
>> c = Category.create(:uuid => '456', :name => 'The Category')
  Category Create (0.5ms)   INSERT INTO "categories" ("name", "created_at", "uuid", "updated_at") VALUES('The Category', '2010-02-04 00:05:30', '456', '2010-02-04 00:05:30')
=> #<Category id: 2, name: "The Category", uuid: "456", created_at: "2010-02-04 00:05:30", updated_at: "2010-02-04 00:05:30">

#Make an Assignment, associating the post and the category
>> a = Assignment.create(:post_uuid => p.uuid, :category_uuid => c.uuid)
  Assignment Create (0.4ms)   INSERT INTO "assignments" ("created_at", "updated_at", "post_uuid", "category_uuid") VALUES('2010-02-04 00:05:50', '2010-02-04 00:05:50', '123', '456')
=> #<Assignment id: 2, post_uuid: "123", category_uuid: "456", created_at: "2010-02-04 00:05:50", updated_at: "2010-02-04 00:05:50">

#Try to fetch the category from the post object, which generates the wrong SQL
>> p.categories
  Category Load (0.0ms)   SQLite3::SQLException: no such column: assignments.uuid: SELECT "categories".* FROM "categories" INNER JOIN "assignments" ON "categories".id = "assignments".uuid WHERE (("assignments".post_uuid = 2)) 
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: assignments.uuid: SELECT "categories".* FROM "categories"  INNER JOIN "assignments" ON "categories".id = "assignments".uuid    WHERE (("assignments".post_uuid = 2)) 
[...Stack trace edited out...]

#Also odd: Try to fetch the post from the Association, no SQL generated, we just get nil
>> a.post
=> nil
A: 

I was able to get your code to work by changing your model definitions.

class Post < ActiveRecord::Base
  set_primary_key :uuid  # tell the model about the non standard primary key
  has_many :assignments, :foreign_key => 'post_uuid'
  has_many :categories, :through => :assignments
end

class Category < ActiveRecord::Base
 set_primary_key :uuid # tell the model about the non standard primary key
 has_many :assignments, :foreign_key => 'category_uuid'
end

class Assignment < ActiveRecord::Base
  belongs_to :post, :foreign_key => 'post_uuid' # notice the value for the foreign_key
  belongs_to :category, :foreign_key => 'category_uuid'  # notice the value for the foreign_key
end

Apart from this, change the migration file to nominate the non standard primary key(uuid) in Category and Post models.

class CreatePosts < ActiveRecord::Migration
  def self.up
    create_table :posts, {:id => false} do |t|
      t.string :uuid, :primary => true, :limit => 20 
      t.string :name
      t.timestamps
    end
  end

  def self.down
    drop_table :posts
  end
end

class CreateCategories < ActiveRecord::Migration
  def self.up
    create_table :categories, {:id => false} do |t|
      t.string :uuid, :primary => true, :limit => 20
      t.string :name

      t.timestamps
    end
  end

  def self.down
    drop_table :categories
  end
end

You can't set the value for uuid via mass assignments while calling new/create method. You have to set the value for the primary_key explicitly.

 # this will not work. uuid will be set to 0
 p = Post.create(:uuid => '123', :name => 'The Post') 

 # this will work
 p = Post.new(:name => 'The Post')
 p.uuid = '123'
 p.save 
KandadaBoggu
Thanks for this. I know I can explicitly declare a different column to be the primary key in my models. But what I'm wondering is, can I make the joins in the associations _without_ changing my primary key on my models?
Gabe Hollombe
Okay. I understand your question now. The SQL statement generated creates as JOIN on categories.id instead of categories.uid. I cant think of solution on top of my head. My solution does solve one of your problems i.e. a.post/a.category being nil. In your Assignment model the belongs_to directive is wrong.
KandadaBoggu
@KandaBoggu, the belongs_to directive changes you mention above only work if I declare post_uuid/category_uuid as the primary key on the post/category models. I'd prefer not to do that in my case. I'm simply asking if there's some way to keep the id columns as the primary key but have AR use a different column for the joins when I specify my relationships.
Gabe Hollombe
The documentation states that foreign_key and class_name parameters are ignored for 'has_many through' associations. I expected the primary_key parameter to be honored. I get stack-overflow error when I change the directive to 'has_many :categories, :through => :assignments, :primary_key => :uuid'. Strange..
KandadaBoggu
A: 

Did you ever find a solution to this problem? I want to do the same thing and AR still doesn't seem to honor the primary_key parameter.

Using the above approach, the id of each return object will be messed with, which isn't ideal when manipulating data.

Jamie