I'm looking for advice here:
I have two tables in legacy scheme, A and B, joined on A.somename = B.othername. Both of those columns are strings. So how do I set up relations between them in rails (v2.1.0)? Given that A has many B's, what would be the best practice:
use :finder_sql and just write a SQL select,
configure the relation through other parameters (how? I know I can set :foreign_key = 'othername', but that will just try to set up a A.id = B.othername relation - what can I do to set up the correct one?),
something else that has not crossed my mind.
So, what would you suggest?
If you're stuck with Rails 2.1 for some reason, the best option seems to be using set_primary_key, like this:
class A
set_primary_key 'somename'
has_many :bs, :foreign_key => 'othername'
end
There is also an alias that lets you use attribution-like syntax for that (self.primary_key = 'somename').
By the way, if you're able to upgrade to 2.3, you can use the primary_key option directly with has_many, like this:
has_many :debitos, :primary_key => 'somename', :foreign_key => 'othername'
If you choose to use this, you won't need to declare the primary key for the class using set_primary_key.
Related
I have a basic database with a structure like this.
products
------------------
id
serial
order
------------------
id
product_serial
Unfortunately, I cannot change structure of the DB. I looked at the docs for Rails 2.1 and it said I could setup a relationship like this.
belongs_to :product,
:class_name => 'Product',
:foreign_key => 'product_serial',
:primary_key => 'serial'
However, that gives me this error.
Unknown key: primary_key
Without the primary key it produces this SQL
SELECT * FROM `products` WHERE (`products`.`id` = #{serial})
How do I setup a belongs_to relationship on this?
EDIT For the record, I am working in Rails 2.1. (I know, don't tell me).
If you check the available options for the belongs_to association for the Rails 2 branch, you'll see that :primary_key is not one of them.
It should be enough, in your case, to simply state the foreign key as you did in the previous line.
Your order model does not have a field (and thus method) which is called serial. You don't need to specify it, then it defaults to id (which you do have).
I have the following models:
Car:
has_many :car_classes
CarClass:
belongs_to :Car
belongs_to :CarMainClass
CarMainClass:
has_many :car_classes
What I want to do is to count the amount of cars in CarClass grouped by the car_main_class_id but then linked to the main_class_symbol which is in CarMainClass.
The query I have now is:
CarClass.group(:car_main_class_id).count(:car_id) => {43=>79, 45=>4 ...}
Which is almost what I want, except that I end up only with the :car_main_class_id which I to be the :main_class_symbol from CarMainClass:
{"A1"=>79, "A2"=>4 ...}
I tried joining the tables and custom select options, but they didn't work.
Can this be done in a query in which I don't have to iterate through the main classes again?
Many thanks for your help!
Instead of having a SQL approach and using a "count/group by", you should look to a very simple feature of Rails ActiveRecords : the counter_cache column.
For example, you can add a column "car_classes_count" in the CarMainClass, and in CarClass class, you do like this :
CarClass:
belongs_to :car
belongs_to :car_main_class, :counter_cache => true
You can do the same with a column "car_class_count" in Car.
I don't know if it can help, but I had the same kind of problems when I started to develop with Rails. I tried to do some unsuccessful crazy SQL queries (queries that worked w/ sqlite, but did not w/ postgres) and I finally choose an other approach.
Try this:
CarClass.includes(:car_main_class => :car_classes)
.group(:car_main_class_id).map { |cc|
{ cc.car_main_class.main_class_symbol => cc.car_main_class.cars.size }
}
Although this is quite ugly - I agree with #Tom that you should try to think of more meaningful class names.
Given a Dinner model that has many Vegetable models, I would prefer that
dinner.vegetables << carrot
not add the carrot if
dinner.vegetables.exists? carrot
Yet it does. It will add a duplicate record every time << is called.
There is a :uniq option you can set on the association, but it only FETCHES AND RETURNS one result if there are multiples, it doesn't ENFORCE unique values.
I could check for exists? every time I add an obj to a collection, but that is tedious and error-prone.
How can I use << freely and not worry about errors and not check for already existing collection members every time?
The best way is to use Set instead of Array:
set = Set.new
set << "a"
set << "a"
set.count -> returns 1
You can add an ActiveRecord unique constraint if you have a join model representing a many-to-many relationship between dinners and vegetables. That's one reason I use join models and has_many :through as opposed to has_and_belongs_to_many. It's important to add a uniqueness constraint at the database level if possible.
UPDATE:
To use a join model to enforce constraint you would need an additional table in your database.
class Dinner
has_many :dinner_vegetables
has_many :vegetables, :through => :dinner_vegetables
end
class Vegetable
has_many :dinner_vegetables
has_many :dinners, :through => :dinner_vegetables
end
class DinnerVegetable
belongs_to :dinner
belongs_to :vegetable
validates :dinner_id, :uniqueness => {:scope => :vegetable_id} # You should also set up a matching DB constraint
end
The other posters' ideas are fine, but as another option you can also enforce this on the database level using e.g. the UNIQUE constraint in MySQL.
After a lot of digging, I've discovered something cool: before_add, which is an association callback, which I never knew even existed. So I could do something like this:
has_many :vegetables, :before_add => :enforce_unique
def enforce_unique(assoc)
if exists? assoc
...
end
Doing this at the DB level is a great idea if you REALLY NEED this to be unique, but in the case that it's not mission critical the solution above is enough for me.
It's mostly to avoid the icky feeling of having extra records lying around in the db...
This seems like a bug in Rails to me, but there's probably not much I can do about that. So how can I accomplish my expected behavior?
Suppose we have:
class User < ActiveRecord::Base
has_many :awesome_friends, :class_name => "Friend", :conditions => {:awesome => true}
end
And execute the code:
>> my_user.awesome_friends << Friend.new(:name=>'jim')
Afterwards, when I inspect this friend object, I see that the user_id field is populated. But I would also expect to see the "awesome" column set to 'true', which it is not.
Furthermore, if I execute the following from the console:
>> my_user.awesome_friends << Friend.new(:name=>'jim')
>> my_user.awesome_friends
= [#<Friend id:1, name:"jim", awesome:nil>]
# Quit and restart the console
>> my_user.awesome_friends
= []
Any thoughts on this? I suppose the conditions hash could be arbitrarily complex, making integration into the setter impossible. But in a way it feels like by default we are passing the condition ":user_id => self.id", and that gets set, so shouldn't others?
Thanks,
Mike
EDIT:
I found that there are callbacks for has_many, so I think I might define the relationship like this:
has_many :awesome_friends,
:class_name => "Friend",
:conditions => {:awesome => true},
:before_add => Proc.new{|p,c| c.awesome = true},
:before_remove => Proc.new{|p,c| c.awesome = false}
Although, it's starting to feel like maybe I'm just implementing some other, existing design pattern. Maybe I should subclass AwesomeFriend < Friend? Ultimately I need a couple of these has_many relationships, and subclassing get's messy with all the extra files..
EDIT 2:
Okay, thanks to everyone who commented! I ultimately wrapped up the method above into a nice little ActiveRecord extension, 'has_many_of_type'. Which works like follows:
has_many_of_type :awesome_friends, :class_name => "Friend", :type=>:awesome
Which just translates to has_many with the appropriate conditions, before_add, and before_remove params (and it assumes the existence of a column named friend_type).
You need use:
my_user.awesome_friends.create(:name=>'jim') or my_user.awesome_friends.build(:name=>'jim')
In documentation:
has_many (:conditions)
Record creations from the association are scoped if a hash is used. has_many :posts, :conditions => {:published => true} will create published posts with #blog.posts.create or #blog.posts.build.
It's :class_name rather than :class, for one thing.
This isn't a bug I don't think. The :conditions hash only deterimines how you query for the objects. But I don't think it's rational to just assume that any object you stuff in the collection could be made to conform to the conditions.
In your simple example it makes sense, but you could also put more complex logic in there.
The documentation seems pretty clear on this as well:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
:conditions
Specify the conditions that the associated object must meet in order to be included as a WHERE SQL fragment, such as authorized = 1.
In my application, a user has_many tickets. Unfortunately, the tickets table does not have a user_id: it has a user_login (it is a legacy database). I am going to change that someday, but for now this change would have too many implications.
So how can I build a "user has_many :tickets" association through the login column?
I tried the following finder_sql, but it does not work.
class User < ActiveRecord::Base
has_many :tickets,
:finder_sql => 'select t.* from tickets t where t.user_login=#{login}'
...
end
I get a weird error:
ArgumentError: /var/lib/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:402:in `to_constant_name': Anonymous modules have no name to be referenced by
from /var/lib/gems/1.8/gems/activerecord-2.0.2/lib/active_record/base.rb:2355:in `interpolate_sql'
from /var/lib/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:214:in `qualified_name_for'
from /var/lib/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:477:in `const_missing'
from (eval):1:in `interpolate_sql'
from /var/lib/gems/1.8/gems/activerecord-2.0.2/lib/active_record/associations/association_proxy.rb:95:in `send'
from /var/lib/gems/1.8/gems/activerecord-2.0.2/lib/active_record/associations/association_proxy.rb:95:in `interpolate_sql'
from /var/lib/gems/1.8/gems/activerecord-2.0.2/lib/active_record/associations/has_many_association.rb:143:in `construct_sql'
from /var/lib/gems/1.8/gems/activerecord-2.0.2/lib/active_record/associations/has_many_association.rb:6:in `initialize'
from /var/lib/gems/1.8/gems/activerecord-2.0.2/lib/active_record/associations.rb:1032:in `new'
from /var/lib/gems/1.8/gems/activerecord-2.0.2/lib/active_record/associations.rb:1032:in `tickets'
from (irb):1
I also tried this finder_sql (with double quotes around the login):
:finder_sql => 'select t.* from tickets t where t.user_login="#{login}"'
But it fails the same way (and anyway, if it worked it would be vulnerable to sql injection).
In a test database, I added a user_id column in the tickets table, and tried this finder_sql:
:finder_sql => 'select t.* from tickets t where t.user_login=#{id}'
Now this works fine. So apparently, my problem has to do with the fact that the users column I am trying to use is a string, not an id.
I searched the net for quite some time... but could not find a clue.
I would love to be able to pass any parameter to the finder_sql, and write things like this:
has_many :tickets_since_subscription,
:finder_sql => ['select t.* from tickets t where t.user_login=?'+
' and t.created_at>=?', '#{login}', '#{subscription_date}']
Edit: I cannot use the :foreign_key parameter of the has_many association because my users table does have an id primary key column, used elsewhere in the application.
Edit#2: apparently I did not read the documentation thoroughly enough: the has_many association can take a :primary_key parameter, to specify which column is the local primary key (default id). Thank you Daniel for opening my eyes! I guess it answers my original question:
has_many tickets, :primary_key="login", :foreign_key="user_login"
But I would still love to know how I can make the has_many :tickets_since_subscription association work.
I think you want the :primary_key option to has_many. It allows you to specify the column on the current Table who's value is stored in the :foreign_key column on the other table.
has_many :tickets, :foreign_key => "user_login", :primary_key => "login"
I found this by reading the has_many docs.
To have something like has_many :tickets_since_subscription you can use named_scopes:
In model add:
named_scope :since_subscription, lambda { |subscription_date| { :conditions => ['created_at > ?', subscription_date] }
With this, you can find what you want like this:
user.tickets.since_subscription 3.days.ago
or
user.tickets.since_subscription user.subscription_date
(of course you need subscription_date column in user model).
You can find more examples here.
If you don't want to use named_scopes you can find what you want with this:
user.tickets.all(:conditions => ['created_at > ?', subscription_date])
I think you are looking for the :foreign_key option on has_many. That should allow you to specify that the foreign key is not user_id, but user_login, without adjusting the finder logic.
See the ActiveRecord has_many documentation for more details.
Just answering to myself, in case there is no better solution. I could not find a solution with the has_many association, so I ended up creating a simple finder method. Not great at all: it does allow me to call some_user.tickets, but it does not give me all the benefits of the has_many associations (namely the clear, delete, <<,... methods on the association itself).
def tickets
return Ticket.find(:all, :conditions=>["user_login = ?", login])
end
I am still hoping that someone will come up with a better solution.