Rails5: How can I use the ActiveRecord "OR" query with "includes"? - activerecord

class Parent < ApplicationRecord
has_many :children
enum status: {
status1: 0,
status2: 1
}
end
class Child < ApplicationRecord
belongs_to :parent
end
# error
# "Relation passed to #or must be structurally compatible. Incompatible values: [:references]"
combination = Parent.status1.or(Parent.status2.includes(:children).where(children: {name: 'ABC'}))
I want to get the data "status1" or "status2 has children named 'ABC'", but error occurs.

The or method takes another relation that has a similar filter pattern, and combines it with the already-existing filters on the object being called.
For example, Parent.status1.or(Parent.status2) would give you a set of records that have either status: 1 or status: 2.
(In case someone is not familiar with it, the example in the question also uses enum, which allows filtering the enum's attribute value using the name of the value. #status1 and #status2 in this case correspond to { status: 0 } and {status: 1} respectively.)
In order to call more relation methods to modify the final result, you must call them on the result of calling #or, like this:
Parent.status1.or(Parent.status2).includes(:children).where(children: {name: 'ABC'})
Based on your comment I see now that you want records that either (have status1) or (have status2 and have a matching children record).
Note that in order to use a relation in a where (like where(children: { name: value }) you must join with the related table (joins(:children).where(children: { name: value }). It seems that ActiveRecord will infer the join if you use only includes, but that's not documented as far as I can tell. This is why or sees the two relations as incompatible: one has children in the references list, while the other does not.
If you write the where clause by hand as a string, it does not change the references list, so or does not see the relation as incompatible. When you write a where clause by hand, you must explicitly use joins:
Parent.status1.joins(:children).or(Parent.status2.joins(:children).where("children.name = 'ABC'"))

You are not calling "includes" on the final or result.
parent = Parent.status1.or(Parent.status2)
parent.includes(:chilren).where(children: {name: "ABC"})

Related

How to get new mongoid indexes for a model

Let's say I have defined my model Person with a couple of indexes:
class Person
include Mongoid::Document
field :email
field :ssn
index({ email: 1 }, { unique: true })
index({ ssn: 1 }, { unique: true })
end
However, only the email index already exists in the database, so when I call
Person.collection.indexes.each {|i| puts i.inspect}
I get the following response:
{"v"=>1, "key"=>{"_id"=>1}, "name"=>"_id_", "ns"=>"x.person"}
{"v"=>1, "unique"=>true, "key"=>{"email"=>1}, "name"=>"email_1", "ns"=>"x.person"}
The question is, how can I get the list of defined indexes in the model, even if they are not already created in mongo ?
In my case, such list should include the definition for the field "ssn"
In other words...How to get those indexes that haven't been created yet ?
Person.index_specifications
shows the indexes defined in the model regardless of its existence in the database.
And
Person.collection.indexes
only shows the index that actually exists in the database.
So there is something else that is worth paying attention to:
rake db:mongoid:create_indexes
will create the indexes defined in the model in the database, and it uses the method 'index_specifications' in deed.
While this removes all the indexes other than the index of the primary key:
rake db:mongoid:remove_indexes
So when you want to only remove the indexes that exists in the database but no longer defined in the database, you should use this:
rake db:mongoid:remove_undefined_indexes
which use the method 'undefined_indexes' in deed.
I hope this can be helpful.
The docs are here:
https://mongoid.github.io/en/mongoid/docs/indexing.html
http://www.rubydoc.info/github/mongoid/mongoid/Mongoid/Tasks/Database#create_indexes-instance_method
Just found it...
We can get the list of all index definitions into the model as follows:
Person.index_specifications
This is an array populated when the application is loaded and is used by the "create_indexes" method as can be seen here:
https://github.com/mongodb/mongoid/blob/master/lib/mongoid/indexable.rb

Rails 4 named scope with record always at end

Is there a way to order records alphabetically, excluding one record, which I want at the end?
class Category < ActiveRecord::Base
default_scope -> { order(:name) }
end
I always want the category named 'Other' to be at the end.
You can try the below ActiveRecord query
Category.order("CASE WHEN name = 'Other' then 0 else 1 END DESC, name ASC")
This is a little bit tricky. In SQL you can add CASE statements to your ORDER BY. In your case the SQL would be something similar to.
SELECT *
FROM categories
ORDER BY
(
CASE
WHEN name = 'Other' THEN 1
ELSE 0
END
)
Here's a live example.
As far as I know, the ActiveRecord order method accepts arbitrary string, so you could (not tested) be able to pass the case to the method
Category.order("CASE WHEN name = 'Other' ... ")
This approach seems complicated, but if you can get it to work is by far the most efficient.
The second alternative is to play a little bit with ActiveRecord.
class Category < ActiveRecord::Base
def self.ordered(condition = nil)
# Get the ID of the record to exclude
excluded = self.where(name: 'Other').pluck(:id).first
# order the records and collect the ids
ids = where('id <> ?', excluded).order(condition).pluck(:id)
# append the excluded at the end
ids << excluded
# recreate the scope and return it.
where(id: ids)
end
end
Category.where(...).ordered
Generally speaking, I encourage you to avoid default_scopes in ActiveRecord. It's so easy to add them, but very hard to remove them when you need.
I would recommend just to add an additional field "position" and sort everything by it. Or you can order by 2 fields (position, name). All Categories will have position equal = 0, and "Other" equal to 999 for example

Access an ActiveRelation in a view

I have two models with the appropriate foreign key created in the people table:
class Person < ActiveRecord::Base
belongs_to :family
class Family < ActiveRecord::Base
has_many :people
If I do the following I get an object - #family_members - as an instance variable and I have no problems:
#family_members = Family.find(1)
I can access the 'child' people table fields easily in my view:
#family_members.people.first_name
However, if I use the arel way with "where" etc. I get an "ActiveRecord::Relation", not a normal object, which leaves me stumped as to how to access the same "first_name" field form the people table like I accessed above:
#family_members = Family.where(:id => 1)
or even
#family_members = Family.joins(:people).where(:id => 1)
(is the "joins" even required??)
I understand that using ".first" will cause the query to run:
#family_members = Family.where(:id => 1).first
But it returns an array, not an object, so if I use in my view:
#family_members.people.first_name
I get a "method 'people' unknown" error.
How can I access the 'first_name' field of the people table like I did with the object created by "find" but using an ActiveRecord relation?
* added information 7/15 ********
To clarify what I am looking for -- here is what I would have written if I were writing SQL instead of Arel:
SELECT f.home_phone, f.address, p.first_name, p.last_name, p.birthday
FROM families f INNER JOIN people p ON p.family.id = f.id WHERE family_id = 1
With that query's results loaded into a result set I could access:
myResultSet("home_phone") -- the home_phone from the families table
myResultSet("address") -- the address from the families table
myResultSet("first_name") -- the first_name from the people table
myResultSet("birthdate") -- the birthdate from the people table
If the two tables in the query have a same-named field I would just use "AS" to request one of the fields by another name.
I have used this kind of query/result set for many years in web apps and I am trying to deduce how to do the same in Rails and ActiveRecord.
#family_members.people.first_name shouldn't ever work so I'm surprised you find it working ... #family_members contains a Family object, #family_members.people is an array of Person objects.
The fact that you're calling it #family_members seems to make me think you're expecting it to be an array of Persons... in which case the correct code would be...
#family_members = Family.find(1).people # finds people in first Family object
If you expect #family_members to contain just the first family member, then...
#family_members = Family.find(1).people.first
If you want an array of first names of all family members, then...
#family_members = Family.find(1).people # finds people in 1st Family object
#family_members.map {|member| member.first_name} # array of first_name
#family_members = Family.find(1) and #family_members = Family.where(:id => 1) are functionally identical.. both retrieve the first Family object in the database in each case may contain zero, one, or multiple people.
Just to be clear, the "1" in all examples above refer to which Family object is retrieved, not which Person in the Family.

Ruby, Retrieve Child Object By Key

I am trying to retrieve a child object based on the key in its parent's table. For instance, I have the Customer class which contains a "store_id" key to the Stores tables. If a customer has a "store_id" key, I would like to bring back that Store object and not the parent Customer object.
EDIT: Here is a sql statement showing what I am trying to do.
So the SQL statement would look something like this.
"SELECT storeS.* FROM customers INNER JOIN stores ON customers.store_id = storeS.id WHERE customers.id = '9'"
I know the sql is probably wrong, but thats a very concise way to show it.
I am assuming you are using rails with the out-of-the-box configuration (using ActiveRecord).
By convention, the "store_id" key in the "customers" table should match an "id" field in the "stores" table. You should also have the following class models setup:
class Store < ActiveRecord::Base
has_many :customers # this is not required for what you want to do here, but recommended
end
class Customer < ActiveRecord::Base
belongs_to :store
end
Assuming this is true, you can either do this if you have the store key:
# assuming we have store key == 9
Store.find(key)
Or you could do this if you already have the customer:
# assuming we have customer.store_id == 9
customer.store
Or if you only have the customer key:
# assuming we have a customer key == 9
customer = Customer.find(9)
store = customer.store
I don't use ActiveRecord a lot, but I think it's this:
Store.find(customer.store_id)

Interacting With Class Objects in Ruby

How can I interact with objects I've created based on their given attributes in Ruby?
To give some context, I'm parsing a text file that might have several hundred entries like the following:
ASIN: B00137RNIQ
-------------------------Status Info-------------------------
Upload created: 2010-04-09 09:33:45
Upload state: Imported
Upload state id: 3
I can parse the above with regular expressions and use the data to create new objects in a "Product" class:
class Product
attr_reader :asin, :creation_date, :upload_state, :upload_state_id
def initialize(asin, creation_date, upload_state, upload_state_id)
#asin = asin
#creation_date = creation_date
#upload_state = upload_state
#upload_state_id = upload_state_id
end
end
After parsing, the raw text from above will be stored in an object that look like this:
[#<Product:0x00000101006ef8 #asin="B00137RNIQ", #creation_date="2010-04-09 09:33:45 ", #upload_state="Imported ", #upload_state_id="3">]
How can I then interact with the newly created class objects? For example, how might I pull all the creation dates for objects with an upload_state_id of 3? I get the feeling I'm going to have to write class methods, but I'm a bit stuck on where to start.
You would need to store the Product objects in a collection. I'll use an array
product_collection = []
# keep adding parse products into the collection as many as they are
product_collection << parsed_product_obj
#next select the subset where upload_state_ud = 3
state_3_products = product_collection.select{|product| product.upload_state_id == 3}
attr reader is a declarative way of defining properties/attributes on your product class. So you can access each value as obj.attribute like I have done for upload_state_id above.
select selects the elements in the target collection, which meet a specific criteria. Each element is assigned to product, and if the criteria evaluates to true is placed in the output collection.

Resources