Get particular object from association with conditions - ruby-on-rails

I have next association for user:
has_many :sent_requests, foreign_key: :requester_id, class_name: 'FriendRequest'
Here is FriendRequest model:
class FriendRequest < ActiveRecord::Base
belongs_to :requester, class_name: 'User'
belongs_to :target, class_name: 'User'
end
I include this association when query requests.
I want to get request to particular user if it exists. Next approach initiates request to the DB:
def friend_request_sent_to(user)
self.sent_requests.where(target: user).first
end
I need to call this method for the list of user. How can I get request to user from self without going to database on every call?

ActiveRecord caching (please be aware of this)
You should be aware that ActiveRecord automatically caches associations. If you evaluate self.sent_requests twice, only one database query will be executed (note, there are some caveats).
That means you can use Enumerable methods multiple times and the association will only be loaded once. E.g.
self.sent_requests.find { |friend_request| friend_request.target == user }
Note that this will not work if you add additional calls to AR query interface methods, like sent_requests.where(...), or sent_requests.joins(...).
Also, ActiveRecord caches query results. So executing self.sent_requests.where(target: user).first multiple times may only result in a single database query.
Memoization
You can also use memoization as suggested by another user. The tricky part about memoizing results is handling nil or false cases. There is a nice gem which handles this for you.
An unsophisticated approach with memoization which avoids nil issues:
class User
has_many :sent_requests, foreign_key: :requester_id, class_name: 'FriendRequest'
def friend_request_sent_to(user)
#sent_requests ||= self.sent_requests.includes(:user).to_a # Will always result in an Array (no need to worry about nil)
#sent_requests.find {|sent_request| sent_request.user == user }
end
This method also has the advantage of allowing you to use more complex queries which would not be cached by ActiveRecord.

You can cache the result in instance variable:
def friend_request_sent?(user)
return #friend_request_sent if defined?(#friend_request_sent)
#friend_request_sent = self.sent_requests.where(target: user).first
end
Also note that it is a ruby convention for methods ending with ? to return true or false. Most likely you rather want to do:
def friend_request_sent?(user)
return #friend_request_sent if defined?(#friend_request_sent)
#friend_request_sent = self.sent_requests.exists?(target: user)
end

Related

Error in custom uniqueness validation for rspec

I am trying to create a rspec test for custom validation in a spree extension(like a gem)
I need to validate uniqueness of a variants
option values for a product (all Spree models)
Here is the basic structure of models(Although they are part of spree, a rails based e-commerce building):
class Product
has_many :variants
has_many :option_values, through: :variants #defined in the spree extension, not in actual spree core
has_many :product_option_types
has_many :option_types, through: :product_option_types
end
class Variant
belongs_to :product, touch: true
has_many :option_values_variants
has_many :option_values, through: option_values
end
class OptionType
has_many :option_values
has_many :product_option_types
has_many :products, through: :product_option_types
end
class OptionValue
belongs_to :option_type
has_many :option_value_variants
has_many :variants, through: :option_value_variants
end
So I have created a custom validation to check the uniqueness of a variants option values for a certain product. That is a product(lets say product1) can have many variants. And a variant with option values lets say (Red(Option_type: Color) and Circle(Option_type: Shape)) have to unique for this product
Anyway this is the custom validator
validate :uniqueness_of_option_values
def uniqueness_of_option_values
#The problem is in product.variants, When I use it the product.variants collection is returning be empty. And I don't get why.
product.variants.each do |v|
#This part inside the each block doesn't matter though for here.
variant_option_values = v.option_values.ids
this_option_values = option_values.collect(&:id)
matches_with_another_variant = (variant_option_values.length == this_option_values.length) && (variant_option_values - this_option_values).empty?
if !option_values.empty? && !(persisted? && v.id == id) && matches_with_another_variant
errors.add(:base, :already_created)
end
end
end
And finally here are the specs
require 'spec_helper'
describe Spree::Variant do
let(:product) { FactoryBot.create(:product) }
let(:variant1) { FactoryBot.create(:variant, product: product) }
describe "#option_values" do
context "on create" do
before do
#variant2 = FactoryBot.create(:variant, product: product, option_values: variant1.option_values)
end
it "should validate that option values are unique for every variant" do
#This is the main test. This should return false according to my uniqueness validation. But its not since in the custom uniqueness validation method product.variants returns empty and hence its not going inside the each block.
puts #variant2.valid?
expect(true).to be true #just so that the test will pass. Not actually what I want to put here
end
end
end
end
Anybody know whats wrong here. Thanks in advance
I have a guess at what's happening. I think a fix would be to change your validation with the following line:
product.variants.reload.each do |v|
What I think is happing is that when you call variant1 in your test, it is running the validation for variant1, which calls variants on the product object. This queries the database for related variants, and gets an empty result. However, since variant2 has the same actual product object, that product object will not re-query the database, and remembers (incorrectly) that its variants is an empty result.
Another change which might make your test run is to change your test as follows:
before do
#variant2 = FactoryBot.create(:variant, product_id: product.id, option_values: variant1.option_values)
end
It is subtle and I'd like to know if it works. This sets the product_id field on variant2, but does not set the product object for the association to be the actual same product object that variant1 has. (In practice this is more likely to happen in your actual code, that the product object is not shared between variant objects.)
Another thing for your correct solution (if all this is right) is to do the reload but put all your save code (and your update code) in a transaction. That way there won't be a race condition of two variants which would conflict, because in a transaction the first must complete the validation and save before the second one does its validation, so it will be sure to detect the other one which just saved.
Some suggested debugging techniques:
If possible, watch the log to see when queries are made. You might have caught that the second validation did not query for variants.
Check the object_id. You might have caught that the product objects were in fact the same object.
Also check new_record? to make sure that variant1 saved before you tested variant2. I think it does save, but it would have be nice to know you checked that.

Preloading of chain of complex/indirect ActiveRecord functions/'associations'

I am working on a plugin for Discourse, which means that I can modify classes with class_eval, but I cannot change the DB schema. To store extra data about the Topic model, I can perform joins with TopicCustomField, which is provided for this purpose.
I am able to store and retrieve all the data I need, but when many Topics are loaded at once, the DB performance is inefficient because my indirect data is loaded once for each Topic by itself. It would be much better if this data were loaded all at once for each Topic, like can happen when using preload or includes.
For example, each Topic has a topic_guid, and a set of parent_guids (stored in a single string with dashes because order is important). These parent_guids point to both other Topic's topic_guids as well as the name of other Groups.
I would love to be able write something like:
has_many :topic_custom_fields
has_many :parent_guids, -> { where(name: 'parent_guids').pluck(:value).first }, :through => :topic_custom_fields
has_many :parent_groups, class_name: 'Group', primary_key: :parent_guids, foreign_key: :name
But this :through complains about not being able to find an association ":parent_guids" in TopicCustomField, and primary_key won't actually take an association instead of a DB column.
I've also tried the following, but the :through clauses are not able to use the functions as associations.
has_many :topic_custom_fields do
def parent_guids
parent_guids_str = where(name: PARENT_GUIDS_FIELD_NAME).pluck(:value).first
return [] unless parent_guids_str
parent_guids_str.split('-').delete_if { |s| s.length == 0 }
end
def parent_groups
Group.where(name: parent_guids)
end
end
has_many :parent_guids, :through => :topic_custom_fields
has_many :parent_groups, :through => :topic_custom_fields
Using Rails 4.2.7.1
Actually, through parameter of rails associations is to set a many-to-many association with a model, passing "through" other model:
http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
So you can't do
has_many :parent_guids, :through => :topic_custom_fields
since ParentGuid is not a model related to TopicCustomFields. Also, pass a block to has_many is only to extend the association with new methods as the ones rails already provide you, like topic_custom_fields.create, topic_custom_fields.build, etc.
Why don't you define the methods inside the block at your second example in the Topic class to retrieve the groups? Is there something you want that would not be possible only with the methods?
Update
Well, I don't think it's possible achieve the same improved performance in this case, since the group ids still have to be handled from topic_custom_fields, and the improved performance is reached through JOINs. Maybe a complex combination of preload, where and references could do the trick, but I don't know if it's possible.
You could try to minimize the db calls instead, maybe gathering all the parent_guids before querying the groups.
I hope there is a more elegant solution, but this is what I have done in order to preload my data efficiently. This should be fairly easy to extend to other applications.
I modify Relation's exec_queries, which calls other preloading functions.
ActiveRecord::Relation.class_eval do
attr_accessor :preload_funcs
old_exec_queries = self.instance_method(:exec_queries)
define_method(:exec_queries) do |&block|
records = old_exec_queries.bind(self).call(&block)
if preload_funcs
preload_funcs.each do |func|
func.call(self, records)
end
end
records
end
end
To Topic, I added:
has_many :topic_custom_fields
attr_accessor :parent_groups
def parent_guids
parent_guids_str = topic_custom_fields.select { |a| a.name == PARENT_GUIDS_FIELD_NAME }.first
return [] unless parent_guids_str
parent_guids_str.value.split('-').delete_if { |s| s.length == 0 }
end
And then in order to preload the parent_groups, I do:
def preload_parent_groups(topics)
topics.preload_funcs ||= []
topics.preload_funcs <<= Proc.new do |association, records|
parent_guidss = association.map {|t| t.parent_guids}.flatten
parent_groupss = Group.where(name: parent_guidss).to_a
records.each do |t|
t.parent_groups = t.parent_guids.map {|guid| parent_groupss.select {|group| group.name == guid }.first}
end
end
topics
end
And finally, I add the preloaders to my Relation query:
result = result.preload(:topic_custom_fields)
result = preload_parent_groups(result)

Ruby on Rails Association build and assign 2 related associations

So I've got a User model, a Building model, and a MaintenanceRequest model.
A user has_many :maintenance_requests, but belongs_to :building.
A maintenance requests belongs_to :building, and belongs_to: user
I'm trying to figure out how to send a new, then create a maintenance request.
What I'd like to do is:
#maintenance_request = current_user.building.maintenance_requests.build(permitted_mr_params)
=> #<MaintenanceRequest id: nil, user_id: 1, building_id: 1>
And have a new maintenance request with the user and building set to it's parent associations.
What I have to do:
#maintenance_request = current_user.maintenance_requests.build(permitted_mr_params)
#maintenance_request.building = current_user.building
It would be nice if I could get the maintenance request to set its building based of the user's building.
Obviously, I can work around this, but I'd really appreciate the syntactic sugar.
From the has_many doc
You can pass a second argument scope as a callable (i.e. proc or lambda) to retrieve a specific set of records or customize the generated query when you access the associated collection.
I.e
class User < ActiveRecord::Base
has_many :maintenance_requests, ->(user){building: user.building}, through: :users
end
Then your desired one line should "just work" current_user.building.maintenance_requests.build(permitted_mr_params)
Alternatively, if you are using cancancan you can add hash conditions in your ability file
can :create, MaintenanceRequest, user: #user.id, building: #user.building_id
In my opinion, I think the approach you propose is fine. It's one extra line of code, but doesn't really increase the complexity of your controller.
Another option is to merge the user_id and building_id, in your request params:
permitted_mr_params.merge(user_id: current_user.id, building_id: current_user.building_id)
#maintenance_request = MaintenanceRequest.create(permitted_mr_params)
Or, if you're not concerned about mass-assignment, set user_id and building_id as a hidden field in your form. I don't see a tremendous benefit, however, as you'll have to whitelist the params.
My approach would be to skip
maintenance_request belongs_to :building
since it already belongs to it through the user. Instead, you can define a method
class MaintenanceRequest
belongs_to :user
def building
user.building
end
#more class stuff
end
Also, in building class
class Building
has_many :users
has_many :maintenance_requests, through: :users
#more stuff
end
So you can completely omit explicit building association with maintenance_request
UPDATE
Since users can move across buildings, you can set automatic behavior with a callback. The job will be done like you do it, but in a more Railsey way
class MaintenanceRequest
#stuff
before_create {
building=user.building
}
end
So, when you create the maintenance_request for the user, the building will be set accordingly

Rails autosave association in controller action

I have the following one to many associations. Document has many Sections and Section has many Items.
class Document < ActiveRecord::Base
has_many :document_sections, :dependent => :destroy, :autosave => true
has_many :document_items, :through => :document_sections
end
class DocumentSection < ActiveRecord::Base
belongs_to :document
has_many :document_items, :dependent => :destroy, :autosave => true
end
class DocumentItem < ActiveRecord::Base
belongs_to :document_section
end
Here is the params hash:
-
Parameters: {"commit"=>"Submit Document", "authenticity_token"=>"4nx2B0pJkvavDmkEQ305ABHy+h5R4bZTrmHUv1setnc=", "id"=>"10184", "document"=>{"section"=>{"10254"=>{"seqnum"=>"3", "item"=>{"10259"=>{"comments"=>"tada"}}}}, "comment"=>"blah"}}
I have the following update method...
# PUT /documents/1
# PUT /documents/1.xml
def update
#document = Document.find(params[:id])
# This is header comment
#document.comment = params[:document][:comment]
params[:document][:section].each do |k,v|
document_section = #document.document_sections.find_by_id(k)
if document_section
v[:item].each do |key, value|
document_item = document_section.feedback_items.find_by_id(key)
if document_item
# This is item comments
document_item.comments = value[:comments]
end
end
end
end
#document.save
end
When I save the document it only updates the document header comments. It does not save the document_item comments. Shouldn't the autosave option also update the associations.
In the log only the following DML is registered:
UPDATE documents SET updated_at = TO_DATE('2010-03-09 08:35:59','YYYY-MM-DD HH24:MI:SS'), comment = 'blah' WHERE id = 10184
How do I save the associations by saving the document.
I think I see what the problem is. I'm pretty sure that you cannot do the following:
# Triggers a database call
document_section = #document.document_sections.find_by_id(k)
And expect ActiveRecord to keep the association for autosaves. Instead, you should save the loaded records individually. Which of course would not be atomic.
I believe for autosave to work like you are thinking, you want to do something like this:
# untested...
#document.document_sections.collect { |s| s.id == k }.foo = "bar"
Notice that here I'm actually modifying a fake param foo in the array, instead of calling find_by_id, which will re-query the database and return a new object.
A third option you have is that you could of course, do what you had originally planned, but handle all the transactions yourself, or use nested transactions, etc, to get the atmoic saves. This would be necessary if your data was too large for array manipulation to work since autosave by it's natures triggers a load of all associated data into memory.
It all depends on your application.
Some clarifications on the underlying problem:
If you run the find_by_id method, you are asking ActiveRecord to return to you a new set of objects that match that query. The fact that you executed that method from an instance (document_sections) is really just another way of saying:
DocumentSection.find_by_id(k)
Calling it from an object instance I think is just some syntactic niceness that rails is adding on the top of things, but in my mind it doesn't make a lot of sense; I think it could be handy in some application, I'm not sure.
On the other side, collect is a Ruby Array method that offers a way to "slice" an array using a block. Basically a fancy foreach loop. :) By interacting with the document_sections array directly, you are changing the same objects already loaded into the containing object (#document), which will then be committed when you save with the special autosave flag set.
HTH! Glad you are back and running. :)

How can I access ActiveRecord Associations in class callbacks in rails?

Updated
Appears to be a precedence error and nothing to do with the question I originally asked. See discussion below.
Original question
Is it possible to use active record associations in callbacks? I've tested this code in the console and it works fine as long as it isn't in a callback. I'm trying to create callbacks that pull attributes from other associated models and I keep getting errors of nil.attribute.
If callbacks are not the correct approach to take, how would one do a similar action in rails? If the associations are simple, you could use create_association(attributes => ), but as associations get more complex this starts to get messy.
For example...
class User < ActiveRecord::Base
belongs_to :b
before_validation_on_create {|user| user.create_b} #note, other logic prevents creating multiple b
end
class B < ActiveRecord::Base
has_many :users, :dependent => destroy
after_create{ |b| b.create_c }
has_one :c
end
class C < ActiveRecord::Base
belongs_to :b
after_create :create_alert_email
private
def create_alert_email
self.alert_email = User.find_by_b_id(self.b_id).email #error, looks for nil.email
end
end
Off course associations are available in your callbacks. After all, the create_after_email is simply a method. You can even call it alone, without using a callback. ActiveRecord doesn't apply any special flag to callback methods to prevent them from working as any other method.
Also notice you are running a User#find query directly without taking advantage of any association method. An other reason why ActiveRecord association feature should not be the guilty in this case.
The reason why you are getting the error should probably searched somewhere else.
Be sure self.b_id is set and references a valid record. Perhaps it is nil or actually there's no User record with that value. In fact, you don't test whether the query returns a record or nil: you are assuming a record with that value always exists. Are you sure this assumption is always statisfied?

Resources