Removing one of the many duplicate entries in a habtm relationship? - ruby-on-rails

For the purposes of the discussion I cooked up a test with two tables:
:stones and :bowls (both created with just timestamps - trivial)
create_table :bowls_stones, :id => false do |t|
t.integer :bowl_id, :null => false
t.integer :stone_id, :null => false
end
The models are pretty self-explanatory, and basic, but here they are:
class Stone < ActiveRecord::Base
has_and_belongs_to_many :bowls
end
class Bowl < ActiveRecord::Base
has_and_belongs_to_many :stones
end
Now, the issue is: I want there to be many of the same stone in each bowl. And I want to be able to remove only one, leaving the other identical stones behind. This seems pretty basic, and I'm really hoping that I can both find a solution and not feel like too much of an idiot when I do.
Here's a test run:
#stone = Stone.new
#stone.save
#bowl = Bowl.new
#bowl.save
#test1 - .delete
5.times do
#bowl.stones << #stone
end
#bowl.stones.count
=> 5
#bowl.stones.delete(#stone)
#bowl.stones.count
=> 0
#removed them all!
#test2 - .delete_at
5.times do
#bowl.stones << #stone
end
#bowl.stones.count
=> 5
index = #bowl.stones.index(#stone)
#bowl.stones.delete_at(index)
#bowl.stones.count
=> 5
#not surprising, I guess... delete_at isn't part of habtm. Fails silently, though.
#bowl.stones.clear
#this is ridiculous, but... let's wipe it all out
5.times do
#bowl.stones << #stone
end
#bowl.stones.count
=> 5
ids = #bowl.stone_ids
index = ids.index(#stone.id)
ids.delete_at(index)
#bowl.stones.clear
ids.each do |id|
#bowl.stones << Stone.find(id)
end
#bowl.stones.count
=> 4
#Is this really the only way?
So... is blowing away the whole thing and reconstructing it from keys really the only way?

You should really be using a has_many :through relationship here. Otherwise, yes, the only way to accomplish your goal is to create a method to count the current number of a particular stone, delete them all, then add N - 1 stones back.
class Bowl << ActiveRecord::Base
has_and_belongs_to_many :stones
def remove_stone(stone, count = 1)
current_stones = self.stones.find(:all, :conditions => {:stone_id => stone.id})
self.stones.delete(stone)
(current_stones.size - count).times { self.stones << stone }
end
end
Remember that LIMIT clauses are not supported in DELETE statements so there really is no way to accomplish what you want in SQL without some sort of other identifier in your table.
(MySQL actually does support DELETE ... LIMIT 1 but AFAIK ActiveRecord won't do that for you. You'd need to execute raw SQL.)

Does the relationship have to be habtm?
You could have something like this ...
class Stone < ActiveRecord::Base
has_many :stone_placements
end
class StonePlacement < ActiveRecord::Base
belongs_to :bowl
belongs_to :stone
end
class Bowl < ActiveRecord::Base
has_many :stone_placements
has_many :stones, :through => :stone_placements
def contents
self.stone_placements.collect{|p| [p.stone] * p.count }.flatten
end
def contents= contents
contents.sort!{|a, b| a.id <=> b.id}
contents.uniq.each{|stone|
count = (contents.rindex(stone) - contents.index(stone)) + 1
if self.stones.include?(stone)
placement = self.stone_placements.find(:first, :conditions => ["stone_id = ?", stone])
if contents.include?(stone)
placement.count = count
placement.save!
else
placement.destroy!
end
else
self.stone_placements << StonePlacement.create(:stone => stone, :bowl => self, :count => count)
end
}
end
end
... assuming you have a count field on StonePlacement to increment and decrement.

How about
bowl.stones.slice!(0)

Related

In Rails, how to count the number of associated records in a collection?

I have a model named Awardunit. Awardunit have many Awardleaders. One Award unit can have one or many Award leaders.
If I get all the record or search and get a collection of records to a variable named awardunits how can I count the number of Awardleaders in all the units in this collection?
Here's what I did :
#leaders = 0
#awardunits.each do |unit|
#leaders = #leaders + unit.awardleaders.size
end
Again to count the disabled leaders I use this :
#disabledleaders = 0
#awardunits.each do |unit|
#disabledleaders = #disabledleaders + unit.awardleaders.where(disabled: true).size
end
If I use this, it will have to go through all the records every time the page loads. Isn't there a better way of doing this?
You can make counting the associations cheap by adding a counter cache:
class AwardUnit << ActiveRecord::Base
has_many :award_units
end
class AwardLeader << ActiveRecord::Base
belongs_to :award_unit, counter_cache: true
end
Now add a new column called award_leaders_count to your AwardUnit table in a new migration:
def change
add_column :award_units, :award_leaders_count, :integer, default: 0
AwardUnit.all.each do |unit|
AwardUnit.reset_counters(unit.id, :award_leaders)
end
end
Rails will now automatically cache the number of award_leaders for every AwardUnit and #my_award_unit.award_leaders.count will give you the count without running another database query.
By default, Rails counter_cache only works for all award_leaders. If you need to count only those award_leaders that have a condition, you will have to add your own counter_cache:
class AwardUnit << ActiveRecord::Base
has_many :award_units
end
class AwardLeader << ActiveRecord::Base
belongs_to :award_unit
scope :disabled, -> { where(disabled: true) }
after_save :update_counter_cache
after_destroy :update_counter_cache
def update_counter_cache
award_unit.update_attribute(:disabled_award_units_count, award_unit.award_leaders.disabled.count)
end
end
migration:
def change
add_column :award_units, :disabled_award_leaders_count, :integer, default: 0
AwardUnit.all.each do |unit|
unit.update_attribute(:disabled_award_units_count, unit.award_leaders.disabled.count)
end
end
Now, when you have an array of AwardUnits, getting their combined count of disabled award leaders is as simple as
#award_units = AwardUnit.limit(5).to_a # or a similar query
#award_units.inject(0){|sum,unit| sum + unit.disabled_award_leaders_count }
You can eager load Awardleaders when fetching Awardunits, so you don't have to execute a count query for every Awardunit, like this:
#awardunits = Awardunit.includes(:awardleaders).where('awardleaders.disabled = ?', true) # the rest of the query
Or, you can query the count directly like this:
#leaders = Awardleader.where('awardunit_id IN (?)', #awardunits.map(&:id)).where(:disabled => true).count
This will run Database query only once
Awardunit.includes(:awardleaders).map {|award_unit| award_unit.awardleaders.size}.inject(0){|sum,item| sum + item }

What is the best syntax for using a `where` statement to find association counts

I can think of a few ways to do this, but I'm unsure what to choose..
I have the class Topic and I am trying to scope this so that I only return Topics if it has the associated object Reply or topic.replies as a count greater than 0.
Worst way to do this :
#topics.select{ | topic | topic.replies > 0 && topic.title == "Conversation" }
Ideally, I'd like to use a where scope.
scope = current_user.topics
scope = scope.joins 'left outer join users on topics.registration_id = registration_members.registration_id'
# scope = .. here I want to exclude any of these topics that have both the title "Conversations" and replies that are not greater than 0
I need to "append" these selections to anything else already selected. So my selection shouldn't exclude all others to just this selection. It's just saying that any Topic with replies less than one and also called "Conversation" should be excluded from the final return.
Any ideas?
Update
Sort of a half-hashed idea :
items_table = Arel::Table.new(scope)
unstarted_conversations = scope.select{|a| a.title == "Conversation" && a.replies.count > 0}.map(&:id)
scope.where(items_table[:id].not_in unstarted_conversations)
You can use something called count cache, basically what it does is add a field to the table and store in that field the total of "associates" of the specified type and is automatically updated.
Checkout this old screen/ascii cast: http://railscasts.com/episodes/23-counter-cache-column?view=asciicast
Here is something newer: http://hiteshrawal.blogspot.com/2011/12/rails-counter-cache.html
In your case would be as follow:
# migration
class AddCounterCacheToTopìc < ActiveRecord::Migration
def self.up
add_column :topics, :replies_count, :integer, :default => 0
end
def self.down
remove_column :topics, :replies_count
end
end
# model
class Replay < ActiveRecord::Base
belongs_to :topic, :counter_cache => true
end
Hope it help you.

Creating multiple habtm relationships with two models in one request

I currently have an Album/Artist database that uses multiple join tables to signify when how an artist relates to the album. They can be listed as a Composer, an Arranger, or a Performer. That is:
class Artist < ActiveRecord::Base
...
has_and_belongs_to_many :albumbycomposers, :class_name => "Album", :join_table => "albums_composers"
has_and_belongs_to_many :albumbyarrangers, :class_name => "Album", :join_table => "albums_arrangers"
has_and_belongs_to_many :albumbyperformers, :class_name => "Album", :join_table => "albums_performers"
...
end
And
class Album < ActiveRecord::Base
has_and_belongs_to_many :composers, :class_name => "Artist", :join_table => "albums_composers"
has_and_belongs_to_many :arrangers, :class_name => "Artist", :join_table => "albums_arrangers"
has_and_belongs_to_many :performers, :class_name => "Artist", :join_table => "albums_performers"
...
end
This code is used to look up for existing artists in the database, then create the association. If no artist exists, then I use the .build method to create the artist.
class AlbumsController < ApplicationController
...
def create
#album = Album.new(params[:album])
params["Composer Names"].each do |each|
if each.empty? == false
#exists = Artist.find_by_name(each)
if #exists.nil? == true
#album.composers.build(:name => each)
else
#album.composers << Artist.find_by_name(each)
end
end
end
params["Arranger Names"].each do |each|
if each.empty? == false
#exists = Artist.find_by_name(each)
if #exists.nil? == true
#album.arrangers.build(:name => each)
else
#album.arrangers << Artist.find_by_name(each)
end
end
end
...
end
...
end
The problem I encounter occurs when I try to enter a new artist as both a composer and an arranger. For example, say I submit this as a post request
Parameters: {"Name"=>["New Album"],
"Performer Names"=>["New Artist"],
"Composer Names"=>["New Artist"],
"Arranger Names"=>["New Artist"],
...
}
Since the composer arguments are first, rails interprets them properly (as if the artist does not exist). The arranger and performer arguments are also interpreted as if the artist does not exist. Then rails begins inserting data into my database. First the album is created and inserted into the albums table, then "New Artist" is created and inserted into album_composer (according to the .build method).
However, for the arranger and performer arguments, the build method can no longer be used, since the artist has been created, so the code is not executed properly.
I tried to workaround by using the push method (aka <<) in the arranger and performer argument lines for this specific case, but that doesn't work because it instantly fires without waiting for the artist to be made by the composer argument, resulting in an "Artist cannot be found" error. For Reference:
collection<<(object, …)
Adds one or more objects to the collection by creating associations in the join table (collection.push and collection.concat are aliases to this method).
Note that this operation instantly fires update sql without waiting for the save or update call on the parent object.
What is the proper way to handle this?
I solved the problem by adding in each artist separately into my database first using the code:
#artists = params["Composer Names"] | params["Arranger Names"] | params["Performer Names"]
#artists.each do |artist|
if artist.empty? == false
#exists = Artist.find_by_name(artist)
if #exists.nil?
#artist = Artist.new(:name => artist)
#artist.save
end
end
end
I could use the push method and add each artist to the proper join table using code like:
params["Composer Names"].each do |each|
if each.empty? == false
#album.composers << Artist.find_by_name(each)
end
end
so that's that!

Using Delta Indexes for associations in Thinking Sphinx

I have a Product model:
class Product < ActiveRecord::Base
belongs_to :subcategory
define_index do
# fields
indexes subcategory.name, :as => :subcategory, :sortable => true, :facet => true
# attributes
has subcategory_id, created_at, updated_at
#properties
set_property :delta => true
Now, suppose that a user updates a subcategory name, which is the proper way to update the products delta index?
According to this documentation: http://freelancing-god.github.com/ts/en/deltas.html, a save message should be sent to the product, so in this case I should go for each product related with the subcategory and send the save message, something like this:
class Subcategory < ActiveRecord::Base
has_many :products
after_save :set_product_delta_flag
private
def set_product_delta_flag
products.each { |product|
product.delta = true
product.save
}
end
end
I think that this is overkilling because we have like 100.000 products per subcategory.
Is this the correct way to update the delta index? Am I missing something?
After adding this:
def set_product_delta_flag
Product.update_all ['delta = ?', true], ['subcategory_id = ?', id]
Product.index_delta
end
I'm always receiving this error:
NoMethodError (undefined method `index_delta' for #):
So, the solution to this problem was to send the message *define_indexes* to the Product model.
After fixing this issue, everything was ok, but the delta_index was not correctly updated, I needed to do save twice to the subcategory model.
So my final solution is this one:
after_commit :set_product_delta_flag
private
def set_product_delta_flag
Product.define_indexes
Product.update_all ['delta = ?', true], ['subcategory_id = ?', id]
Product.index_delta
end
Using after_commit and define_indexes is the correct solution? Its the only one that I've found.
Try the following instead:
def set_product_delta_flag
Product.update_all ['delta = ?', true], ['subcategory_id = ?', id]
Product.index_delta
end
A single SQL statement, a single delta re-indexing. Should perform far better :)

Implementing rental store in Rails: how to track an inventory state over time?

Let's say you're implementing rails app for a snowboard rental store.
A given snowboard can be in one of 3 states:
away for maintenance
available at store X
on loan to customer Y
The company needs to be able to view a rental history for
a particular snowboard
a particular customer
The rental history needs to include temporal data (e.g. Sally rented snowboard 0123 from Dec. 1, 2009 to Dec. 3 2009).
How would you design your model? Would you have a snowboard table with 4 columns (id, state, customer, store), and copy rows from this table, along with a timestamp, to a snowboard_history table every time the state changes?
Thanks!
(Note: I'm not actually trying to implement a rental store; this was just the simplest analogue I could think of.)
I would use a pair of plugins to get the job done. Which would use four models. Snowboard, Store, User and Audit.
acts_as_state_machine and acts_as_audited
AASM simplifies the state transitions. While auditing creates the history you want.
The code for Store and User is trivial and acts_as_audited will handle the audits model.
class Snowboard < ActiveRecord::Base
include AASM
belongs_to :store
aasm_initial_state :unread
acts_as_audited :only => :state
aasm_state :maintenance
aasm_state :available
aasm_state :rented
aasm_event :send_for_repairs do
transitions :to => :maintenance, :from => [:available]
end
aasm_event :return_from_repairs do
transitions :to => :available, :from => [:maintenance]
end
aasm_event :rent_to_customer do
transitions :to => :rented, :from => [:available]
end
aasm_event :returned_by_customer do
transitions :to => :available, :from => [:rented]
end
end
class User < ActiveRecord::Base
has_many :full_history, :class_name => 'Audit', :as => :user,
:conditions => {:auditable_type => "Snowboard"}
end
Assuming your customer is the current_user during the controller action when state changes that's all you need.
To get a snowboard history:
#snowboard.audits
To get a customer's rental history:
#customer.full_history
You might want to create a helper method to shape a customer's history into something more useful. Maybe something like his:
def rental_history
history = []
outstanding_rentals = {}
full_history.each do |item|
id = item.auditable_id
if rented_at = outstanding_rentals.keys.delete(id)
history << {
:snowboard_id => id,
:rental_start => rented_at,
:rental_end => item.created_at
}
else
outstanding_rentals[:id] = item.created_at
end
end
history << oustanding_rentals.collect{|key, value| {:snowboard_id => key,
:rental_start => value}
end
end
First I would generate separate models for Snowboard, Customer and Store.
script/generate model Snowboard name:string price:integer ...
script/generate model Customer name:string ...
script/generate model Store name:string ...
(rails automatically generates id and created_at, modified_at dates)
To preserve the history, I wouldn't copy rows/values from those tables, unless it is necessary (for example if you'd like to track the price customer rented it).
Instead, I would create SnowboardEvent model (you could call it SnowboardHistory if you like, but personally it feels strange to make new history) with the similiar properties you described:
ev_type (ie. 0 for RETURN, 1 for MAINTENANCE, 2 for RENT...)
snowboard_id (not null)
customer_id
store_id
For example,
script/generate model SnowboardEvent ev_type:integer snowboard_id:integer \
customer_id:integer store_id:integer
Then I'd set all the relations between SnowboardEvent, Snowboard, Customer and Store. Snowboard could have functions like current_state, current_store implemented as
class Snowboard < ActiveRecord::Base
has_many :snowboard_events
validates_presence_of :name
def initialize(store)
ev = SnowboardEvent.new(
{:ev_type => RETURN,
:store_id => store.id,
:snowboard_id = id,
:customer_id => nil})
ev.save
end
def current_state
ev = snowboard_events.last
ev.ev_type
end
def current_store
ev = snowboard_events.last
if ev.ev_type == RETURN
return ev.store_id
end
nil
end
def rent(customer)
last = snowboard_events.last
if last.ev_type == RETURN
ev = SnowboardEvent.new(
{:ev_type => RENT,
:snowboard_id => id,
:customer_id => customer.id
:store_id => nil })
ev.save
end
end
def return_to(store)
last = snowboard_events.last
if last.ev_type != RETURN
# Force customer to be same as last one
ev = SnowboardEvent.new(
{:ev_type => RETURN,
:snowboard_id => id,
:customer_id => last.customer.id
:store_id => store.id})
ev.save
end
end
end
And Customer would have same has_many :snowboard_events.
Checking the snowboard or customer history, would be just a matter of looping through the records with Snowboard.snowboard_events or Customer.snowboard_events. The "temporal data" would be the created_at property of those events. I don't think using Observer is necessary or related.
NOTE: the above code is not tested and by no means perfect, but just to get the idea :)

Resources