Sunspot / Solr / Rails: Model Associations are not updating in the Index - ruby-on-rails-3

I have a Fieldnote model in my app, which has_many :activities attached to it through a table called :fieldnote_activities. I then define a searchable index this way:
searchable :auto_index => true, :auto_remove => true do
integer :id
integer :user_id, :references => User
integer :activity_ids, :multiple => true do
activities.map(&:id)
end
text :observations
end
And then I have a Search model to store / update searches. The search model thus also has its own associations with activities. I then perform my searches like this:
#search = Search.find(params[:id])
#query = Fieldnote.search do |query|
query.keywords #search.terms
if #search.activities.map(&:id).empty? == false
query.with :activity_ids, #search.activities.map(&:id)
end
end
#fieldnotes = #query.results
Now this all works GREAT. The problem is that if I change which activities that are associated with a fieldnote, the search results do not change because it appears the indices for that fieldnote do not change. I was under the impression that the :auto_index => true and :auto_remove => true flags when I define the searchable index would keep track of new associations (or deleted associations), but this appears not to be the case. How do I fix this?

You're right that :auto_index and :auto_remove don't apply to associated objects, just the searchable object they are specified on.
When denormalizing, you should use after_save hooks on the associated objects to trigger a reindex where necessary. In this case, you want changes to the Activity model and the FieldnoteActivity join model to trigger a reindex of their associated Fieldnote objects when saved or destroyed.
class Fieldnote
has_many :fieldnote_activities
has_many :activities, :through => :fieldnote_activities
searchable do
# index denormalized data from activities
end
end
class FieldnoteActivity
has_many :fieldnotes
has_many :activities
after_save :reindex_fieldnotes
before_destroy :reindex_fieldnotes
def reindex_fieldnotes
Sunspot.index(fieldnotes)
end
end
class Activity
has_many :fieldnote_activities
has_many :fieldnotes, :through => :fieldnote_activities
after_save :reindex_fieldnotes
before_destroy :reindex_fieldnotes
def reindex_fieldnotes
Sunspot.index(fieldnotes)
end
end

Related

How to rewrite Rails associations without separate queries (looping through them again)

Running into some performance issues with the following code (stripped out irrelevant parts).
This is the CardsController#index code:
def index
cards = cards.paginate(page: index_params[:page], per_page: limit)
# Assign bumped attribute
cards.each do |card|
if current_user
card.bumped = card.bump_by?(current_user)
card.bump = card.get_bump(current_user)
else
card.bumped = false
card.bump = nil
end
end
end
Card.rb:
class Card < ActiveRecord::Base
belongs_to :cardable, polymorphic: true, touch: true
belongs_to :user
has_many :card_comments, autosave: true
has_many :card_bumps
has_many :card_bumpers, through: :card_bumps, class_name: 'User', source: :user
def bump_by?(user)
self.card_bumpers.include? user
end
def get_bump(user)
CardBump.find_by(user_id: user.id, card_id: self.id)
end
end
How can I avoid and optimize the second loop on each card where I do the associations of card.bumped and card.bump ?
Thanks in advance
In the model level, since the method bump_by? equals to bump existence, so
card.bumped = !card.bump.empty?
So the whole includes check in method bump_by? can be avoided, which in turn avoid fetching all associated bumps.
First of all, I would optimize your controller code a little and would update all cards with a single query if current_user is not present:
def index
cards = cards.paginate(page: index_params[:page], per_page: limit)
# Assign bumped attribute
if current_user
cards.each do |card|
card.bump = card.get_bump(current_user)
card.bumped = card.bump_by?(current_user)
end
else
cards.update_all(bump: nil, bumped: false)
end
end
There is also a possibility to optimize model code:
class Card < ActiveRecord::Base
belongs_to :cardable, polymorphic: true, touch: true
belongs_to :user
has_many :card_comments, autosave: true
has_many :card_bumps
has_many :card_bumpers, through: :card_bumps, class_name: 'User', source: :user
def bump_by?(user)
# use #exists? to check whether a record is present in the database.
# This will make a `SELECT 1 as count...` query and,
# therefore, perform a lookup on database level.
# #include? in opposite will load ALL associated items from DB,
# turn them into a ruby objects array and perform a lookup in the
# obtained array which is much slower than simple lookup performed by #exists?
get_bump(user) == user || self.card_bumpers.exists?(user.id)
end
def get_bump(user)
#_bump ||= self.card_bumpers.find_by(user_id: user.id)
end
end
Since Card#get_bump is also looking in card_bumpers association we can memorize its result and later use memorized value in Card#bump_by? without hitting database again. If there is no memorized value then fast check for record existence will be performed by a database.
Notice, that I changed lines order in controller to get benefit of memorizing:
card.bump = card.get_bump(current_user)
card.bumped = card.bump_by?(current_user)

Rails saving data from associated form

How can I get the data from an associated form and insert it to the associated table from the main model?
class Supplier < ActiveRecord::Base
has_one :account, foreign_key: "acc_sup_id", :autosave => true
self.primary_key = 'sup_id'
end
class Account < ActiveRecord::Base
belongs_to :supplier, foreign_key: "acc_sup_id"
self.primary_key = 'acc_id'
self.table_name = 'accounts'
end
I am having a combined form for Supplier and Account. When I submit I need to find a way to insert the corresponding values to Supplier and Account. The problem is Supplier values is inserting properly but not Account.
I have asked the same question in several forums, groups and even in stack but nobody seems to give a convincing answer.
The basic strategy is to first look at what params are being submitted when the form is submitted. You could add a line in the controller action such as raise params.inspect to see that. Make sure that those paras contain all the data you need; if not then there is some problem in the view that generates that form.
Once you have all the data getting to the controller action, then you need to change the controller action so that is properly interprets all the data and puts it into the correct models.
I cannot give any more specific advice unless you show the code for your view, the result from doing params.inspect, and the code for the controller action that takes the data.
Try this.
Let's assume that there are orders and customers tables and that you want to perform CRUD operations on customers from orders form.
Customer model is very simple
class Customer < ActiveRecord::Base
attr_accessible :name
end
Order model must provide virtual attributes for all customer's attributes (attr_accessor construct). CRUD for customers is provided through callbacks. Validations can be used as well.
class Order < ActiveRecord::Base
attr_accessor :customer_name
attr_accessible :description, :number, :customer_name
belongs_to :customer
validates_presence_of :number
validates_presence_of :description
validates_presence_of :customer_name
before_save :save_customer
after_find :find_customer
after_destroy :destroy_customer
protected
def save_customer
if self.customer
self.customer.name = self.customer_name
else
self.customer = Customer.create(name: self.customer_name)
end
self.customer.save
end
def find_customer
self.customer_name = self.customer.name
end
def destroy_customer
self.customer.destroy
end
end
Example grid for Order model.
class Orders < Netzke::Basepack::Grid
def configure(c)
super
c.model = 'Order'
c.items = [
:description,
:number,
:customer_name
]
c.enable_edit_inline = false
c.enable_add_inline = false
end
def preconfigure_record_window(c)
super
c.form_config.klass = OrderForm
end
end
Example form for Order model.
class OrderForm< Netzke::Basepack::Form
def configure(c)
super
c.model = 'Order'
c.items = [
:description,
:number,
:customer_name
]
end
end

Rails ActiveRecord define database objects within other object

I'm relatively new to rails and very new to database manipulation.
I'm trying to create a class within the database which contains a number of custom objects within it. These custom objects are also to be stored in the database in a separate table. I've managed to set this up as follows
class MyClass < ActiveRecord::Base
has_many :other_objects, :dependent => destroy
end
class OtherObject < ActiveRecord::Base
belongs_to :my_class
attr_accessible :some_stuff...
end
I've created the appropriate database tables and managed to get it working.
Now what I want to do is have (four) particular instances of "OtherObject"s in my class, which can be accessed by some straightforward identifier, something like
test = MyClass.new
...
test.instance_of_other_object.some_attribute = "blahblah"
Such that this updates the database entry for the associated object. What is the best way to go about this?
That has_many association sets up MyClass#other_objects (and a bunch of other methods) to allow you to easily work with associated records.
You probably want:
my_class.other_objects.each do |other_object|
other_object.update_attributes(:foo => 'bar')
end
If you want a direct SQL update, you can use update_all:
my_class.other_objects.update_all(:foo => 'bar')
Update:
If that's the sort of association you need, you may define a belongs_to association:
class MyClass < ActiveRecord::Base
has_many :other_objects, :dependent => :destroy
# uses :selected_other_object_id
belongs_to :selected_other_object, :class_name => "OtherObject"
end
my_class = MyClass.first
my_class.selected_other_object = other_object # Set the object.
# => #<OtherClass:...>
my_class.selected_other_object_id # ID has been set.
# => 10
my_class.selected_other_object # Retrieve the object.
# => #<OtherClass:...>
my_class.selected_other_object.save # Persist ID and other fields in the DB.
my_class = MyClass.find(my_class.id) # If you fetch the object again...
# => #<MyClass:...>
my_class.selected_other_object_id # The ID is still there.
# => 10
my_class.selected_other_object # You have the ID, you get the object.
# => #<OtherClass:...>
my_class.selected_other_object.foo = "bar" # Access associated object this way.
another_variable = my_class.selected_other_object # Or this way.
Remember however that this does not assume that :selected_other_object is a subset of :other_objects.
Also note that the selected_other_object and selected_other_object= methods are already set up when you set up the association, so you don't have to define these yourself.
This is not a complete answer but I have come up with something that works for getting the object, but not for setting it.
class MyClass < ActiveRecord::Base
has_many :other_objects, :dependent => destroy
def particular_instance
return OtherObject.find_by_id(self.particular_instance_id)
end
end
Where my db schema looks like this
create_table "my_classs", :force => true do |t|
t.integer "particular_instance_id"
end
create_table "other_objects", :force => true do |t|
t.integer "my_class_id"
t.string "some_attribute"
end
Update
In order to set an attribute of the other_object class the update_attributes method can be used
my_class.particular_instance.update_attributes(:some_attribute => "blah")
Use object.dup now for rails 3.1 and on.
a = MyClass.first # finds the first instance of the model "MyClass"
b = a.dup # duplicates the record.
c = a.dup # duplicates the record again.
b.field_name = "blahblah"
c.fielf_name = "blahblah"
b.save # saves the record into the database for object b.
c.save # saves the record into the database for object c.
If you look into your database, you can see that a new record has been created. It is identical to the first record excepted that it has a new id.
Also check Duplicate a model for more information about it.

best way to save dependent objects in a has_and_belongs_to_many relation?

Hi I am new to rails and I would like to know what is the best way who save dependent objects in an HBTM relation.
Specifically, I have two classes Post and Tag
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts
end
I have a migration to create the joining table
class AddPostsTagsJoinTable < ActiveRecord::Migration
def self.up
create_table :posts_tags, :id => false do |t|
t.integer :post_id
t.integer :tag_id
end
end
def self.down
drop_table :postss_tags
end
end
All is good up to here
So I have a PostsController from which I handle the creation, updates and deletes for the posts, and I want to encapsulate the Tags so that the creation is via the PostsController... like so:
class PostsController < ApplicationController
#... code removed for brevity
def create
#post = current_user.posts.build(params[:post])
if #post.save
tag_names = params[:post][:tags].strip.split(' ')
tag_names.each do |t|
#see if the tag already exists
tag = Tag.find_by_name(t);
if tag.nil?
#post.tags.create!(:name => t)
else
#post.tags << tag #just create the association
end
end
flash[:success] = "Post created."
redirect_to(user_posts_path(current_user.username))
else
#user = current_user
render 'new'
end
end
end
I am not sure how I should handle the creation of my Tag(s) because if I just call
#post.tags.create!(:name => t)
this will create duplicate records in the Tags table (even when :uniq => true is specified in the model).
So to avoid the duplication I see if a tag is already present and then add it like this
tag = Tag.find_by_name(t);
if tag.nil?
#post.tags.create!(:name => t)
else
#post.tags << tag #just create the association
end
Is this the way it's supposed to be done?
This seems expensive (especially 'cause it's in a loop) so I am wondering if there is another "cleaner" way to do this? (pls forget the DRY'ing up of the action and so on)
Is there a clean way to create my Tags without having to manually check for duplicates?
thank you in advance for your help!
You can save tags attribute of post if automatically by adding accepts_nested_attributes_for to Post model
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
accepts_nested_attributes_for :tags
end
The next step is to output tags fields inside post form.

Custom return value for new / create ActiveRecord model

So I have a model (Photo), where when I call Photo.new #image => #image / Photo.create :image => #image, I want my model to find an existing photo with the same image hash OR create a new Photo from #image. Assume I can't use Photo.find_or_initialize_by_hash because I have a custom find function which finds close copies of images based on a soft image hash.
My first idea was to do
before_validation :check_duplicates, :on => :create
def check_duplicates
self = self.find_duplicate
end
Unfortunately, I realized you can't just redefine self in a model, so now I think the best approach is doing something along the lines of changing the return value from initialize to the duplicate.
Sort of like this, but it doesn't work (and I've heard horror stories about overriding initialize)
def initialize(*params)
super(*params)
return self.find_duplicate || self
end
From what I gather your model structure looks something like this?
class Photo < ActiveRecord::Base
has_one :image
end
class Image < ActiveRecord::Base
belongs_to :photo
end
If so, you can simply do this:
class Photo < ActiveRecord::Base
has_one :image, :uniq => true
end
Or if :image is just an attribute of Photo your first idea was on track:
class Photo < ActiveRecord::Base
before_create :check_duplicate
private
def check_duplicate
Photo.where(:image => self.image).count == 0 # will be false if Photo is found
end
end
which will cancel the Photo from being created if #check_duplicate returns false (http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html)
Or simply
class Photo < ActiveRecord::Base
validates_uniqueness_of :image
end