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.
Related
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.
I have a question about rails and how its relationships query builder, specifically how camel case is converted for the related calls.
Relevant Code
class CustomerPlan < ActiveRecord::Base
attr_accessible :customer_id, :plan_id, :startDate, :user_id
has_many :planActions
end
class PlanAction < ActiveRecord::Base
attr_accessible :actionType_id, :customerPlan_id, :notes, :timeSpent
belongs_to :customerPlan
belongs_to :actionType
end
The getters and setters work just fine, such as plan_action.actionType.name will correctly pull from the related model. However customer_plan.planActions.each returns the error:
SQLite3::SQLException: no such column: plan_actions.customer_plan_id:
SELECT "plan_actions".*
FROM "plan_actions"
WHERE "plan_actions"."customer_plan_id" = 1
The column is defined in the database as customerPlan_id, was I just wrong to use this? It works for every other call, all my other relationships work fine. Even PlanAction -> CustomerPlan.
I ran through all the docs, and searched about every other source I know of. It would be simple enough to change my columns, I just want to know what's going on here.
Thank you for your time!
A quick fix for this is to just explicitly set the foreign_key.
has_many :planActions, :foreign_key => "customerPlan_id", :class_name => "PlanAction"
Still, I think I am missing some model naming convention somewhere, just can't seem to figure out what.
The Rails convention for DB column names is to use lowercase letters with words separated by an underscore (e.g. author_id, comments_count, updated_at, etc).
I would highly recommend that you stick to the Rails conventions. This would make your life much easier. To change it to the rails convention, simply create a migration to rename the column to the appropriate style.
However, if you do want to use a custom style for the column name, rails provides the :foreign_key option in the has_many relationship to specify the expected foreign column name:
class CustomerPlan < ActiveRecord::Base
has_many :plan_actions, :foreign_key => 'customerPlan_id'
end
You can also use the alias_attribute macro to alias the column name, if you'd like to use a different model attribute name than the actual DB column name. But as I mentioned, I would recommend sticking to the rails convention as much as possible. You'll thank me later.
Rails has 3 basic naming schemes.
One is for constants, and it is ALL_UPPERCASE_SEPARATED_BY_UNDERSCORES.
One is for Classes and it is AllCamelCaseWithNoUnderscores.
One is for variables and method names, and is all_lowercase_separated_by_underscores.
The reason that it is this way is not just for consistency, but also because it freely converts between them using these methods.
So, to make your posted code more rails-y:
class CustomerPlan < ActiveRecord::Base
attr_accessible :customer_id, :plan_id, :start_date, :user_id
has_many :plan_actions
end
class PlanAction < ActiveRecord::Base
attr_accessible :action_type_id, :customer_plan_id, :notes, :time_spent
belongs_to :customer_plan
belongs_to :action_type
end
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
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
I have googled myself almost to death over this and the closest I came to anything similar is this stack overflow question (which attempts to ask several questions at once). I have only one. OK, two - but providing an answer to the first will get your answer accepted as long as it adheres to the requirements below.
I am using Rails 3 and Ruby 1.8.7 with a legacy database. The only thing that is up for debate is the Rails version. I am stuck with Ruby 1.8.7 and the database structure.
Here are the significant portions of the models involved:
class List < ActiveRecord::Base
set_primary_key "ListID"
has_many :listitem, :foreign_key => "ListID", :dependent => :destroy
has_many :extra_field, :foreign_key => "ListID", :dependent => :destroy
end
class Listitem < ActiveRecord::Base
set_table_name "ListItems"
set_primary_key "ListItemID"
belongs_to :list
has_many :list_item_extra_field, :foreign_key => 'ListItemID', :dependent => :destroy
end
This is what I get in rails console:
irb(main):001:0> List.joins(:listitem).to_sql
=> "SELECT [lists].* FROM [lists] INNER JOIN [ListItems] ON [ListItems].[ListID] IS NULL"
When I am expecting a sql statement more like:
SELECT [lists].* FROM [lists] INNER JOIN [ListItems] ON [ListItems].[ListID] = [Lists].[ListID]
Getting me to the query above will get a correct answer. Bonus points if you can tell me how to get to something equivalent to:
SELECT [lists].*, COUNT([ListItems].*) FROM [lists] INNER JOIN [ListItems] ON [ListItems].[ListID] = [Lists].[ListID]
Your first question: you probably messed with table migration. In migration file use
create_table :lists, {:id => false} do
....
end
and then add execute "ALTER TABLE lists ADD PRIMARY KEY (listid);"
And by the way, it's has_many :listitems (plural, not single).
Look here for detailed explanation: Using Rails, how can I set my primary key to not be an integer-typed column?
Second question: I would use combination of methods, not one method. YOu need to return name of list and then count name of list items in this list, right? After solving the first problem, simply use list.name and list.list_items.count
As usual, the answer is ridiculously simple with Rails...
My tables are named using mixed case. Adding the following line to my List model fixed it:
set_table_name "Lists"
The plurality (or lack thereof) of :listitem(s) appeared to have no effect before or after this change.