How to specify polymorphic association with custom primary keys in Rails - ruby-on-rails

I have a few ActiveRecord classes using paper_trail for version tracking. The AR classes have custom primary keys based on their table names (e.g. Item.ItemID instead of Item.id) in order to adhere to business DB conventions.
paper_trail specifies a polymorphic relationship on each of the tracked classes, thus:
class TrackedExample < ActiveRecord::Base
set_table_name 'TrackedExample'
set_primary_key 'TrackedExampleID'
# simplified from https://github.com/airblade/paper_trail/blob/master/lib/paper_trail/has_paper_trail.rb
has_many :versions
:class_name => 'Version'
:as => :item,
end
class AnotherTrackedExample
set_table_name 'AnotherTrackedExample'
set_primary_key 'AnotherTrackedExampleID'
has_many :versions
:class_name => 'Version'
:as => :item,
end
# from https://github.com/airblade/paper_trail/blob/master/lib/paper_trail/version.rb
class Version
belongs_to :item, :polymorphic => true
...
end
If I were not using custom primary keys, the version object could refer to the tracked object (i.e. the object of which it is a version) using Version#item. When I try it, I get an error:
# This should give back `my_tracked_example`
my_tracked_example.version.first.item
=> TinyTds::Error: Invalid column name 'id'.: EXEC sp_executesql N'SELECT TOP (1) [TrackedExample].* FROM [TrackedExample] WHERE [TrackedExample].[id] = 1 ORDER BY TrackedExample.TrackedExampleID ASC'
Is there a way to get Version#item to perform the correct query? I would expect something like this:
EXEC sp_executesql N'SELECT TOP (1) [TrackedExample].* FROM [TrackedExample] WHERE [TrackedExample].[TrackedExampleID] = 1 ORDER BY TrackedExample.TrackedExampleID ASC'
I'm using Rails 3.1.0, paper_trail 2.6.4 and MS SQL Server through TinyTDS and activerecord-sqlserver-adapter.
EDIT: I've worked around the problem by adding computed columns TrackedExample.id and AnotherTrackedExample.id that refer to the primary key values. This isn't a proper solution (Rails is still making the wrong query), but it may be useful to others in a hurry.
MS SQL:
ALTER TABLE TrackedExample
ADD COLUMN id AS TrackedExampleID

Rather than specifying t.references :item, polymorphic: true in your migration file.
You simply have to specify the item_id and item_type as follows:
t.string :item_id
t.string :item_type
Rails will automatically select the correct primary_key for the item_id and correct type.

I haven't tried it, but this might work
class Version
belongs_to :item, :polymorphic => true, :primary_key => 'TrackedExampleID'
...
end

After scouring the PaperTrail documents it doesn't look like you can override what column item_id references (i.e. the primary key of the Item's table), so I think you have two main options:
1. Create an id column that does not have the name of the class in it. So, id instead of TrackedExampleID.
You said you already did this as a quick fix.
2. Fork and patch PaperTrail to allow you to pass what column to use when querying for item_id.
This could either be the value set with set_primary_key 'TrackedExampleID' or it could be something set like has_paper_trail primary_key: 'TrakedExampleID'.
Let us know what you end up with.

Related

Rails 4 has_one self reference column

I have a Totem model and Totems table.
There will be many totems and I need to store the order of the totems in the database table.
I added a previous_totem_id and next_totem_id to the Totems table to store the order information. I did it via this
Rails Migration:
class AddPreviousNextTotemColumnsToTotems < ActiveRecord::Migration
def change
add_column :totems, :previous_totem_id, :integer
add_column :totems, :next_totem_id, :integer
end
end
Now in the Model I have defined the relationships:
class Totem < ActiveRecord::Base
validates :name, :presence => true
has_one :previous_totem, :class_name => 'Totem'
has_one :next_totem, :class_name => 'Totem'
end
I created a couple of these totems through ActiveRecord and tried to use the previous_totem_id column like so:
totem = Totem.create! name: 'a1'
Totem.create! name: '1a'
totem.previous_totem_id = Totem.find_by(name: '1a').id
puts totem.previous_totem #This is NIL
However, the previous_totem comes back as nil, and I do not see a select statement in the mysql log when calling this line
totem.previous_totem
Is this relationship recommended? What is the best way to implement a self referencing column?
Changing direction of the association from has_one to belongs_to and specifying foreign keys, should make your code work as you expected:
class Totem < ActiveRecord::Base
validates :name, :presence => true
belongs_to :previous_totem, :class_name => 'Totem', foreign_key: :previous_totem_id
belongs_to :next_totem, :class_name => 'Totem', foreign_key: :next_totem_id
end
However, good association should be properly named and declared on both sides - with matching has_one association; in this case it's impossible without naming conflicts :) Self join might be sometimes useful, but i'm not sure if it's the best solution here. I didn't use the gem moveson recommends, but an integer column to store position is something I use and IMHO makes reordering records easier :)
If the only reason for the self-reference is to store the order of the totems, please don't do it this way. It's your lucky day: This is a solved problem!
Use a position field and the acts_as_list gem, which will take care of this problem for you in a neat and performant way.

is it possible to customize foreign key to use a regex

We have a simple item and order model.
orders(url)
items(title, orders_count)
Where "url" can be any URL from the web for e.g. http://amzn.to/aCKiXO. What we would like to do is that if the user enters "/items/7" for the "url" then have it behave like a foreign key. So something like:
class Order < ActiveRecord::Base
belongs_to :item, :foreign_key => :url, :regex => /items/n,
:counter_cache => true
end
class Item < ActiveRecord::Base
has_many :orders, :foreign_key => :url, :regex => /items/n,
:dependent => :destroy
end
Is this possible? We're on Rails 2.3.8, Ruby 1.9.3 and on Postgresql 9.1
No, it isn't possible to add a foreign key constraint that does a regular expression match - or anything except simple equality. See the PostgreSQL documentation on constraints.
What you can do is any of:
Write a constraint trigger in PL/PgSQL to enforce the constraint you want;
Split out the part you want to add a constraint on using a regexp in the application, and define a foreign key constraint on a column containing only that part; or
Use a BEFORE INSERT OR UPDATE ... FOR EACH ROW trigger to split the part of interest out in PL/PgSQL when the row is inserted and add the part of interest to a foreign key column that contains only that part. The app doesn't need to know about the duplication since the DB is taking care of it behind the scenes.

How to deal with xor conditions, rails, foreign keys and a sqlite database?

What I want is, building this somehow with Rails 3.1:
If A has set an id for b_id, it shouldn't be possible to setting an id for c_id. And for sure vice versa too.
I wish I could do at the database level from a migration (check constraint?). Is this somehow possible?
Or is it more affordable to do this in the model with validations?
My environment:
Ruby 1.9.3
Rails 3.1.3
SQLite 3.7.3
You can accomplish this through polymorphic associations, altho the schema won't look exactly like what you have, you can accomplish the same goal, having an item A belong to either a B or a C but never to both.
You can read more here: http://guides.rubyonrails.org/association_basics.html#polymorphic-associations
In the example given on that link, A is their Picture, and Employee and Proudct are your B and C:
(copied from source linked above):
class Picture < ActiveRecord::Base
belongs_to :imageable, :polymorphic => true
end
class Employee < ActiveRecord::Base
has_many :pictures, :as => :imageable
end
class Product < ActiveRecord::Base
has_many :pictures, :as => :imageable
end
I would definitely write validations for this - it's easier to provide good error messages to a user from a validations. I'd also like to back it up with a database constraint. It looks like check constraints can indeed do the job.
Rails has no support for this that I could find so you'll need to create the table with raw sql. You'll also need to change the schema dumper to :sql as rails won't be able to produce a schema.rb that describes this actually.
I wrote this migration
class CreateFoos < ActiveRecord::Migration
def change
execute <<SQL
CREATE TABLE foos (
id INTEGER PRIMARY KEY,
x_id INTEGER,
y_id INTEGER,
constraint xorit check( (x_id OR y_id) AND NOT(x_id AND y_id))
)
SQL
end
end
Then in the rails console
Foo.create(:x_id => 1, :y_id => 1) #=> SQLite3::ConstraintException
As it is you can create a row with neither x_id nor y_id set. You could change this by changing the constraint,
(x_id IS NOT NULL OR y_id IS NOT NULL ) AND (x_id IS NULL OR y_id IS NULL)
seemed to work for me

HABTM - uniqueness constraint

I have two models with a HABTM relationship - User and Role.
user - has_and_belongs_to_many :roles
role - belongs_to :user
I want to add a uniqueness constraint in the join (users_roles table) that says the user_id and role_id must be unique. In Rails, would look like:
validates_uniqueness_of :user, :scope => [:role]
Of course, in Rails, we don't usually have a model to represent the join relationship in a HABTM association.
So my question is where is the best place to add the constraint?
You can add uniqueness to join table
add_index :users_roles, [ :user_id, :role_id ], :unique => true, :name => 'by_user_and_role'
see In a join table, what's the best workaround for Rails' absence of a composite key?
Your database will raise an exception then, which you have to handle.
I don't know any ready to use rails validation for this case, but you can add your own validation like this:
class User < ActiveRecord::Base
has_and_belongs_to_many :roles, :before_add => :validates_role
I would just silently drop the database call and report success.
def validates_role(role)
raise ActiveRecord::Rollback if self.roles.include? role
end
ActiveRecord::Rollback is internally captured but not reraised.
Edit
Don't use the part where I'm adding custom validation. It kinda works but there is better alternatives.
Use :uniq option on association as #Spyros suggested in another answer:
class Parts < ActiveRecord::Base
has_and_belongs_to_many :assemblies, :uniq => true, :read_only => true
end
(this code snippet is from Rails Guides v.3). Read up on Rails Guides v 3.2.13 look for 4.4.2.19 :uniq
Rails Guide v.4 specifically warns against using include? for checking for uniqueness because of possible race conditions.
The part about adding an index to join table stays.
In Rails 5 you'll want to use distinct instead of uniq
Also, try this for ensuring uniqueness
has_and_belongs_to_many :foos, -> { distinct } do
def << (value)
super value rescue ActiveRecord::RecordNotUnique
end
end
I think that using :uniq => true would ensure that you get no duplicate objects. But, if you want to check on whether a duplicate exists before writing a second one to your db, i would probably use find_or_create_by_name_and_description(...).
(Of course name and description are your column values)
I prefer
class User < ActiveRecord::Base
has_and_belongs_to_many :roles, -> { uniq }
end
other options reference here

Do I need to manually create a migration for a HABTM join table?

I'm struggling now to get HATBM working correctly. I have a beaten scanario: articles and tags. I presume, HABTM should be used here, since it is a many-to-many relationship.
I don't know however if I should manually create a join table (articles_tags in this case).
My code currently as follows:
class Article < ActiveRecord::Base
has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :articles
end
When I run the migrations, no 3rd table is created.
Also, I would like to add that my third table doesn't bear any domain logic, just blind assignment.
I'm using Rails 2.2.2
You should do this in a migration of one of the tables, or in a separate migration if those migrations have been ran:
create_table :articles_tags, :id => false do |t|
t.references :article, :tag
end
add_index :articles_tags, [:article_id, :tag_id]
This will create the table for you and the :id => false tells Rails not to add an id field to this table. There's an index also, which will speed up lookups for this join table.
You could also generate a model (ArticlesTag) for this and do:
# article.rb
has_many :articles_tags
has_many :tags, :through => :articles_tags
# tag.rb
has_many :articles_tags
has_many :articles, :through => :articles_tags
# article_tag.rb
belongs_to :tag
belongs_to :article
And then create the table in the migration generated from the script/generate model articles_tag call.
Note that this is covered in the API.
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_and_belongs_to_many
You probably also want to add an index to the migration:
add_index "articles_tags", "article_id"
add_index "articles_tags", "tag_id"
However, if you want tagging functionality I'd recommend the acts_as_taggable_on rails plugin:
http://www.intridea.com/tag/acts_as_taggable_on
http://github.com/mbleigh/acts-as-taggable-on/
I've used it on a project and it was very easy to implement.
One of the issues with a join table for tagging is that it can easily get ugly creating a join table for each content type you wish to make taggable (ie. comments_tags, posts_tags, images_tags, etc). This plugin uses a taggings table which includes a discriminator to determine the content type without the need of a specific join table for each type.
In combination with this Qeuestion(1st answear) How to set up a typical users HABTM roles relationship and 1st answear from here, it has to be understood even by a monkey. I am new in RoR and it's got working like a charm

Resources