How to query for Mongoid documents by two references - ruby-on-rails-4

I'm using Mongoid on this project. Lets say I have a model like so:
class Voice
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Enum
include Mongoid::Paperclip
enum :status, [:enabled, :disabled], :validate => true, :default => :enabled
...
has_and_belongs_to_many :categories
has_and_belongs_to_many :builder_types
has_and_belongs_to_many :voice_types
has_and_belongs_to_many :preferences
has_and_belongs_to_many :languages
embeds_many :comments
embeds_many :ratings
belongs_to :artist, :class_name => "User"
...
end
As you can see, Voice has and belongs to Category, BuilderType, VoiceType and so on. At the moment, if I want to search for all voices that belongs to a specific category I'd do the following (pseudo code):
#category = Category.find(id)
#voices = #category.voices
Which works fine. How about search for Voice that has or belong to multiple relations and relations type .e.g. (pseudo code that doesn't work):
#cat1 = Category.find(id)
#cat2 = Category.find(id)
#voice_type = VoiceType.find(id)
#voices = #cat1.voices.where(category_id: #cat2.id).where(voice_type_id: #voice_type.id)
But that doesn't work. Any ideas if 1) that's at all possible and 2) how could I do it?

This may work out for you:
Voice.all(category_ids: [#cat1.id, #cat2.id]).
where(:voice_type_ids.in [#voice_type.id])

For future reference, at the end what I did was:
#voices = Voice.all(category_ids: [#cat_1.id, #cat_2.id]).
and(voice_type_ids: #voice_type.id).map{ |voice| voice.format_frontend }

Related

Ruby on Rails Unique Ordered Query

What I am trying to do is:
Find the most recent CompetencyLog (completed_at) for each Competency on a Course for a Member
class Member < ActiveRecord::Base
has_many :competency_logs
has_many :awards
end
class CompetencyLog < ActiveRecord::Base
belongs_to :member
belongs_to :competency
has_one :course, through: :competency
end
def Course < ActiveRecord::Base
has_many :competencies
has_many :awards
end
class Competency < ActiveRecord::Base
belongs_to :course
end
I have managed to get the ordered list
course = Course.find(params[:course_id])
current_member.competency_logs.where('competency_id IN (?)', course.competency_ids).ordered
From here I have tried a few different things with limited to no success. Any recommendations would be greatly appreciated. Looking to do as much of this in the database as possible for speed since this is frequently called and depended on ever changing timestamps on the CompetencyLog
The results I want are basically
member.competency_logs.where('competency_id IN (?)', course.competency_ids).uniq.pluck(:competency_id)
But instead of the competency_id I want the competency_log models
So I've added some additional relationships and come up with the following so far, currently investigating postgresql DISTINCT ON as an alternative
competency_logs = competencies.collect { |c| c.competency_logs.ordered.first }
competency_logs.collect { |c| c.current? }.all?
The following will work:
max_completed = current_member.competency_logs.where('competency_id IN (?)', course.competency_ids).maximum(:completed_at)
current_member.competency_logs.first(:conditions => {:completed_at => max_completed})
This might solve your problem. It does however operate under the assumption that you don't have duplicated completed_at timestamps per member.
some_member.competency_logs.
where(
competency_id: some_course.competency_ids,
completed_at: some_member.competency_logs.
select("MAX(completed_at) as completed_at").
where(competency_id: some_course.competency_ids).
group(:completed_at)
)

Can I make a belongs_to association use eager loading by default?

I am connecting to one of my company's SQL Server databases, and trying to set up ActiveRecord so I can treat them just the same as Rails objects.
I have these two models:
class Change < ActiveRecord::Base
belongs_to :affected_contact, class_name: "Contact"
end
class Contact
# Contact's primary key is a binary UUID; I can't change this
end
I am trying to get the affected contact of one particular change. Normally, this would be a simple case, but:
Change.first.affected_contact
Change Load (52.6ms) EXEC sp_executesql N'SELECT TOP (1) [chg].* FROM [chg] ORDER BY [chg].[id] ASC'
Contact Load (28.0ms) EXEC sp_executesql N'SELECT TOP (1) [ca_contact].* FROM [ca_contact] WHERE [ca_contact].[contact_uuid] = #0', N'#0 binary', #0 = 0xfcf9a8ac6381aa4386c9b10ee382e10b [["contact_uuid", "<16 bytes of binary data>"]]
=> nil
... that's not what I want! And yet, if I eager-load the join first, it works:
Change.eager_load(:affected_contact).first.affected_contact
SQL (34.4ms) EXEC sp_executesql N'SELECT TOP (1) holy_crap_theres_a_lot_of_columns FROM [chg] LEFT OUTER JOIN [ca_contact] ON [ca_contact].[contact_uuid] = [chg].[affected_contact] ORDER BY [chg].[id] ASC'
=> #<Contact contact_uuid: "\xFC\xF9\xA8\xACc\x81\xAAC\x86\xC9\xB1\x0E\xE3\x82\xE1\v", ... >
In fact, if I force the matching to happen in the JOIN clause in any way, it will work, but belongs_to seems to use the WHERE clause instead, and nil is the best response I can get (a lot of the time, there are conversion errors between the string and its binary type).
Is there a way to ensure eager-loading through the JOIN clause happens by default on the belongs_to association?
I found that #find_by_contact_uuid (contact_uuid being the primary key) worked, where #find didn't, for some reason. That led to this being implemented.
I have ended up essentially rewriting the association methods that Active Record supplies:
module AssociationMethods
def self.included(base)
base.reflect_on_all_associations(:belong_to).each do |a|
define_method a.name do
# #find_by_<uuid_pk> seems to work where #find doesn't
a.klass.send "find_by_#{a.association_primary_key}", self[a.foreign_key]
end
end
base.reflect_on_all_associations(:has_many).each do |a|
define_method a.name do
a.klass.where(a.foreign_key => self.send(a.association_primary_key))
end
end
end
end
class Contact
has_many :changes, foreign_key: :affected_contact_id
include AssociationMethods # include *after* all associations are defined
end
class Change
belongs_to :affected_contact, class_name: 'Contact'
include AssociationMethods
end
It doesn't cover everything that Active Record supplies when setting up the associations, but it seems to do the trick.
Using includes should resolve your problem. This is because includes will preload or eager_load depending on your other conditions.
read more here

Rails Sort Results based on an association to a child model's method

I've got an Event model. and this Event model has_one Address model. A User model also has_one Address model. I want to get a list of all events in the future, but sorted based on distance between the events address and the users address. This 'distance' is a method on my address model. how can i write the rails query with active record? I've tried many variations of the following but nothing seems to work.
Event.where('end_time > ?', Time.now).includes(Address).order(address.distance_from(User.find(3).primary_address))
i always get this error: NameError: undefined local variable or method `address' for main:Object
definitions of Event and User:
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
has_many :addresses
belongs_to :primary_address, :class_name => "Address"
class Event < ActiveRecord::Base
extend TimeFromPieces
attr_accessor :start_date_string
belongs_to :address
delegate :timezone, to: :address
here is my address model:
class Address < ActiveRecord::Base
belongs_to :user
has_many :events
validates_presence_of :name, :address1, :city, :state, :zip
geocoded_by :address_as_string do |obj, results|
Rails.logger.debug { "results: #{results}" }
obj.lat = results.first.coordinates[0]
obj.lng = results.first.coordinates[1]
Rails.logger.debug { "Neighborhoods: #{results.first.address_components_of_type(:neighborhood)}" }
neighborhoods = results.first.address_components_of_type(:neighborhood)
if neighborhoods.count > 1
obj.neighborhood = neighborhoods.first["long_name"]
end
end
after_validation :geocode, :if => :address1_changed?
after_validation :time_zone_for_address, :if => :address1_changed?
before_save :set_primary_if_only_address_for_user
before_save :normalize_fields
scope :near, ->(lat, lng, distance_in_meters = 2000) {
where(%{
ST_Dwithin(
ST_GeographyFromText(
'SRID=4326;POINT(' || addresses.lng || ' ' || addresses.lat || ')'
),
ST_GeographyFromText('SRID=4326;POINT(%f %f)'),
%d
)
} % [lng, lat, distance_in_meters])
}
def self.distance(address1, address2)
query = %{ SELECT
ST_Distance(
ST_GeographyFromText('SRID=4326;POINT(%f %f)'),
ST_GeographyFromText('SRID=4326;POINT(%f %f)')
)
} % [address1.lng, address1.lat, address2.lng, address2.lat]
meters = ActiveRecord::Base.connection.execute(query).values.first.first.to_i
meters/1609.34
end
def distance_from(address)
Address.distance(self, address)
end
end
Instead of using a ruby geocoder, you could use a gem like geokit-rails, which provides geocoding abilities to active record, it gives you nice methods that allow you to query by distance directly to the database, so you don't need to fetch all objects to calculate their distance
You could say that address is a mappable object, and point the lat and lng fields ( if they aren't using the default names used by the gem )
class Address
acts_as_mappable
end
Then you could say that an event is also mappable through the address object attached to it
class Event
acts_as_mappable through: :address
end
The query would look something like this
Event.where('end_time < ?', Time.now).by_distance(origin: address)

Using ActiveModel::Serializer with a model that has several references onto the same object

I'm trying to create a serializer that will give out an object that has several references onto the same object. For example there is an "Lender" object that has two addresses, one of them is the "registration address" and the second is "actual address".
Model
class Lender < ActiveRecord::Base
...
belongs_to :address, foreign_key: 'registration_address_id'
belongs_to :address, foreign_key: 'actual_address_id'
...
end
Serializer
class LenderSerializer < ActiveModel::Serializer
embed :ids, include: true
attributes :id, ...
has_one :address, key: :actual_address_id
has_one :address, key: :registration_address_id
end
When client loads this object from the server there is only one reference included in the JSON output just like this:
{"addresses":[{"id":5,"full_address":"..."}],
"lenders":[{"id":2,"company_title":null,"registration_address_id":5}]}
The problem is that in the output JSON there is no "actual_address_id" and there is no "actual address" in the first array of addresses.
Just in case if someone will need it.
In order to resolve this, I've switched onto JBuilder gem, that gives much more explicit control on the output JSON format.

Strong parameters for nested attributes returns "unpermitted parameters" when empty array

Assuming a User model
using Rails4 with strong_parameters.
class User < ActiveRecord::Base
has_secure_password
accepts_nested_attributes_for :identity
// rest of code omitted for brevity
end
If I refer to the guide I should be able to do
def user_params
params.require(:user).permit(:email, identity_attributes: [])
end
to allow mass_assignment of each identity_attributes whatever their names or number. But this run in a "Unpermitted parameters: identity_attributes"
But if I specify the identity_attributes it works
def user_params
params.require(:user).permit(:email, identity_attributes: [:last_name, :first_name])
end
I have many attributes in Identity, I would be able to mass_assign them through User without specifying all of them.
Am I missing something ? Is it a bug ?
Cheers
You have to specify the identity's attributes you want to updated, including the :id of the identity entity.
you will have something like that :
def user_params
params.require(:user).permit(:email, identity_attributes: [:id, :last_name, :first_name])
end
if you don't specify the :id, Rails will try to create an entity instead of updating it. I spend all the week-end struggling on a simple one-to-many relationship using accepts_nested_attributes_for because I didn't specified the id in the permitted attributes.