I'm working on a legacy database that is complete non-sense. I have a table called movie that contains columns with names like c00, c01, c02 and so on. The table also uses non-standard primary_keys. So I've created a class called movie like this:
class Movie < ActiveRecord::Base
set_table_name "movie"
set_primary_key "idMovie"
belongs_to :media_file, :foreign_key => "idFile"
def title
self.c00
end
def plot
self.c01
end
end
I'd like to be able to do something like Movie.find_by_title("Die Hard") and have it return the right result. Also I'd like to be able to say Movie.create(:title => "Die Hard"). How do I do this?
I think you want alias_attribute. Check out Brian Hogan's excellent presentation from RailsConf this year.
You really just need a combination of Sarah's answer and Ben's answer:
class Movie < ActiveRecord::Base
# gives you Movie.find_by_title
# and lets you chain with other named scopes
named_scope :find_by_title, lambda { |title| { :conditions => { :c00 => title } } }
# gives you
# movie.title
# movie.title=(title)
# and
# Movie.new(:title => title)
alias_attribute :title, :c00
end
The find_by_* methods use reflection, so it just isn't going to happen with Rails out-of-the-box. You can, of course, define your own methods:
def self.find_by_title(title)
first(:conditions => { :c00 => title })
end
The next step would be to iterate over a hash of column_aliases => real_columns to use as fodder for calls to alias_attribute and define_method.
Related
I made a real basic github project here that demonstrates the issue. Basically, when I create a new comment, it is saved as expected; when I update an existing comment, it isn't saved. However, that isn't what the docs for :autosave => true say ... they say the opposite. Here's the code:
class Post < ActiveRecord::Base
has_many :comments,
:autosave => true,
:inverse_of => :post,
:dependent => :destroy
def comment=(val)
obj=comments.find_or_initialize_by(:posted_at=>Date.today)
obj.text=val
end
end
class Comment < ActiveRecord::Base
belongs_to :post, :inverse_of=>:comments
end
Now in the console, I test:
p=Post.create(:name=>'How to groom your unicorn')
p.comment="That's cool!"
p.save!
p.comments # returns value as expected. Now we try the update case ...
p.comment="But how to you polish the rainbow?"
p.save!
p.comments # oops ... it wasn't updated
Why not? What am I missing?
Note if you don't use "find_or_initialize", it works as ActiveRecord respects the association cache - otherwise it reloads the comments too often, throwing out the change. ie, this implementation works
def comment=(val)
obj=comments.detect {|obj| obj.posted_at==Date.today}
obj = comments.build(:posted_at=>Date.today) if(obj.nil?)
obj.text=val
end
But of course, I don't want to walk through the collection in memory if I could just do it with the database. Plus, it seems inconsistent that it works with new object but not an existing object.
Here is another option. You can explicitly add the record returned by find_or_initialize_by to the collection if it is not a new record.
def comment=(val)
obj=comments.find_or_initialize_by(:posted_at=>Date.today)
unless obj.new_record?
association(:comments).add_to_target(obj)
end
obj.text=val
end
I don't think you can make this work. When you use find_or_initialize_by it looks like the collection is not used - just the scoping. So you are getting back a different object.
If you change your method:
def comment=(val)
obj = comments.find_or_initialize_by(:posted_at => Date.today)
obj.text = val
puts "obj.object_id: #{obj.object_id} (#{obj.text})"
puts "comments[0].object_id: #{comments[0].object_id} (#{comments[0].text})"
obj.text
end
You'll see this:
p.comment="But how to you polish the rainbow?"
obj.object_id: 70287116773300 (But how to you polish the rainbow?)
comments[0].object_id: 70287100595240 (That's cool!)
So the comment from find_or_initialize_by is not in the collection, it outside of it. If you want this to work, I think you need to use detect and build as you have in the question:
def comment=(val)
obj = comments.detect {|c| c.posted_at == Date.today } || comments.build(:posted_at => Date.today)
obj.text = val
end
John Naegle is right. But you can still do what you want without using detect. Since you are updating only today's comment you can order the association by posted_date and simply access the first member of the comments collection to updated it. Rails will autosave for you from there:
class Post < ActiveRecord::Base
has_many :comments, ->{order "posted_at DESC"}, :autosave=>true, :inverse_of=>:post,:dependent=>:destroy
def comment=(val)
if comments.empty? || comments[0].posted_at != Date.today
comments.build(:posted_at=>Date.today, :text => val)
else
comments[0].text=val
end
end
end
I have an Update model which belongs to users.
To show all of one user's friends' Updates, I am doing something like:
Update.where("user_id" => [array_of_friend_ids])
I know the "right" way of doing things is to create a method to create the above array. I started writing the method but it's only half-working. Currently I have this in my user model:
def self.findfriends(id)
#friendarray = []
#registered_friends = Friend.where("user_id" => id)
#registered_friends.each do |x|
#friendarray << x.friend_id
end
return #friendarray
end
I am doing the entire action in the view with:
<% #friendinsert = User.findfriends(current_user.id) %>
<% #friendarray = [] %>
<% #friendarray << #friendinsert %>
<%= #friendarray.flatten! %>
Then I'm calling Update.where("user_id" => #friendarray) which works. But obviously I'm doing things in a very hacky way here. I'm a bit confused as to when Rails can "see" certain variables from models and methods in the view. What's the best way to go about inserting an array of IDs to find their Updates, since I'm not supposed to use much logic in the view itself?
Mattharick is right about using associations. You should use associations for the question you mentioned in description of your question. If we come to the question at the title of your question;
let's say you have a User model.
These two methods are different:
def self.testing
puts "I'm testing"
end
and the other one is:
def testing
puts "I'm testing"
end
Pay attention to the self keyword. self keyword makes method a Class method. Which you can call it from your controllers or views like: User.testing.
But the one with out testing is a instance method. Which can be called like:
u = User.last
u.testing
Second one gives you possibility to use attributes of the 'instance' inside your model.
For example, you can show name of your instance in that method just like this?
def testing
puts "Look, I'm showing this instance's name which is: #{name}"
end
These are powerful stuff.
Practise on them.
Simple add another association to your project.
class User < ActiveRecord::Base
has_many :friendship
has_many :friends, :through => :friendship, :class_name => User, :foreign_key => :friend_id
has_many :friendship
has_many :users, :through => :friendship
end
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, :class_name => User
end
I don't know if my synrax is correct, please try out.
Friendship has the attributes user_id and friend_id.
After that you should be able to do something like following to get the updates of a friend:
User.last.friends.last.updates
You can work with normal active record queries instead of hacky arrays..
We are using thinkingtank gem and having trouble indexing model associations, even simple ones. For example, a profile belongs to an institution, which has a name – we would like to do something like:
class Profile < ActiveRecord::Base
#model associations
define_index do
indexes institution(:name), :as => :institution_name
end
end
but that doesn't work. This must be very simple – what am I doing wrong?
a possible solution to this issue would be adding a method returning the element to index. For the profile.institution.name case:
# profile.rb
# ...
belongs_to :institution
# ...
define_index do
indexes institution_name
end
def institution_name
self.institution.name
end
# ...
Also the ", :as => ..." syntax is not supported on thinkingtank.
I would also recommend giving a try to Tanker: https://github.com/kidpollo/tanker
Regards.
Adrian
I'm having some issues in RoR with some model methods I am setting. I'm trying to build a method on one model, with an argument that gets supplied a default value (nil). The ideal is that if a value is passed to the method, it will do something other than the default behavior. Here is the setup:
I currently have four models: Market, Deal, Merchant, and BusinessType
Associations look like this:
class Deal
belongs_to :market
belongs_to :merchant
end
class Market
has_many :deals
has_many :merchants
end
class Merchant
has_many :deals
belongs_to :market
belongs_to :business_type
end
class BusinessType
has_many :merchants
has_many :deals, :through => :merchants
end
I am trying to pull some data based on Business Type (I have greatly simplified the return, for the sake of brevity):
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.sum('price')
else
return self.deals(:conditions => ['market_id = ?',market]).sum('price')
end
end
end
So, if I do something like:
puts BusinessType.first.revenue
I get the expected result, that is the sum of the price of all deals associated with that business type. However, when I do this:
puts BusinessType.first.revenue(1)
It still returns the sum price of all deals, NOT the sum price of all deals from market 1. I've also tried:
puts BusinessType.first.revenue(market=1)
Also with no luck.
What am I missing?
Thanks!
Try this:
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.all.sum(&:price)
else
return self.deals.find(:all, :conditions => ['market_id = ?',market]).sum(&:price)
end
end
end
That should work for you, or at least it did for some basic testing I did first.
As I have gathered, this is because the sum method being called is on enumerable, not the sum method from ActiveRecord as you might have expected.
Note:
I just looked a bit further, and noticed you can still use your old code with a smaller tweak than the one I noted:
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.sum('price')
else
return self.deals.sum('price', :conditions => ['market_id = ?', market])
end
end
end
Try this!
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.sum(:price)
else
return self.deals.sum(:price,:conditions => ['market_id = ?',market])
end
end
end
You can refer this link for other functions. http://en.wikibooks.org/wiki/Ruby_on_Rails/ActiveRecord/Calculations
I have the following situation
class RecordA
has_many :recordbs
end
class RecordB
belongs_to :recorda
end
RecordA has many recordbs but only one of them may be an active recordb. I need something like myRecordA.active_recordb
If I add a new column like is_active to RecordB, then I have the potential problem of setting two records to is_active = true at the same time.
Which design pattern can I use?
Thanks!
Let's change your example. There's a LectureRoom, with many People and only one Person can be the instructor.
It'd be much easier to just have an attribute in the LectureRoom to indicate which Person is the instructor. That way you don't need to change multiple People records in order to swap the instructor. You just need to update the LectureRoom record.
I would use a named scope to find the active lecturer.
class Person
named_scope :currently_speaking, :conditions => {:active => true}
end
Then I would call that a lecturer in ClassRoom:
class ClassRoom
def lecturer
people.currently_speaking.first
end
end
The real problem is making sure that when you activate someone else, they become the only active person. I might do that like this:
class Person
belongs_to :class_room
before_save :ensure_one_lecturer
def activate!
self.active = true
save
end
def ensure_one_lecturer
if self.active && changed.has_key?(:active)
class_room.lecturer.update_attribute(:active, false)
end
end
end
This way everything is done in a transaction, it's only done if you've changed the active state, and should be pretty easily tested (I have not tested this).
You can define a class method on RecordB for this:
class RecordB < ActiveRecord::Base
def self.active
first(:conditions => { :active => true }
end
end