How to write code to NOT include null value fields into mongodb - ruby

class State
include Mongoid::Document
embeds_many :cities
field :name
end
class City
include Mongoid::Document
embedded_in :state
field :name
field :population
field ...
end
I don't want to include the fields with nil value into mongodb,
nsw = State.new name: 'NSW'
if number_of_people
nsw.cities.create name: 'Syndey', population: number_of_people
else
nsw.cities.create name: 'Syndey'
end
so it is necessary to check whether or not that field is empty or null. But the problem is when there are many fields in City, the code looks ugly.
How to improve this and write smart code?

You need to define a custom class method in City model like the following:
def self.create_persistences(fields = {})
attributes = {}
fields.each do |key, value|
attributes[key] = value if value
end
create attributes
end
and in your controller, call this method without conditions hassle:
nsw.cities.create_persistences name: 'Syndey', population: number_of_people
note: you can also override create method on your model instead of defining new method but in my opinion, I don't prefer to override something you may use in other part of the code.

Now we know what you are doing your answer seems clear. But I think your question needs an edit to inform.
So what you have is data from some source that you are using to populate your new model. So at some stage here you are going to have a hash or at least some way of constructing a hash in some form from however your data is organized. Take the following [short form but the same thing]:
info = { name: "Sydney", population: 100 }
City.new( info );
info = { name: "Melbourne", population: 80, info: "fun" }
City.new( info )
info = { name: "Adelaide" }
City.new( info )
So (at least in my testing ), you are going to get each document, with only the specified fields created each time.
So dynamically using the hash (and hopefully you are even just reading in that way ) is going to be a lot smarter than testing each value in code.
If you have to do a lot of value testing to even "build up" a hash then you have problems that no-one here can fix. But building hashes should be easy.

Related

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

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"})

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

Mongoid's .includes() Not Populating Relations

I am using Mongoid v4.0.2, and I'm running into an interesting issue using .includes(). I have a record that represents invoices, who has a list of charges.
I want to query for a single invoice and have the charges be populated after I run the query. According to the docs (search for "Eager Loading"), I should be able to do something like this to have Mongoid populate the charges:
Invoice.includes(:charges).find_by({ _id: <objectId> })
When I get the record back the charges are still showing up as a list of ObjectId's, and removing the .includes() seems to have no effect one way or another. I've verified each charge exists in the record I'm querying for, so I'm confused why they aren't populating.
I believe I have the data models set up correctly, but I'll include them here for completeness.
class Invoice
include Mongoid::Document
has_many :charges
field :status, type: String
field :created, type: Time, default: -> { Time.now }
end
class Charge
include Mongoid::Document
field :created, type: Time, default: -> { Time.now }
field :transactionId, type: String
field :category, type: String
field :amount, type: Float
field :notes, type: String
belongs_to :invoices
end
There is no reason to use includes if you are only finding one document. Just find the document and then access the relation. Either way, 2 database requests will be issued.
The only time includes provides a performance increase is when you are loading multiple relations for multiple documents, because what Mongoid will do is load the queried documents, go through and gather all of the ids that should be queried for all of those documents and then query for all relations as one database call using the :id.in => ids feature. In your case, there is no point to do this.

How can I validate a DateTime field

I had two fields available_to and available_from in a model called Hotel. When creating a new hotel, we got to specify the date and time! I want to validate these two fields, in such a way that, if one selects available_to as 23/2/2015 , the available_from should not be 23/2/2014.
The fastest way is creating a custom validation method within model itself.
Try updating Hotel as follows:
class Hotel < ActiveRecord::Base
validate :validate_available_to if Proc.new { |h| h.available_from && h.available_to }
def validate_available_to
return if available_to > available_from
errors.add(:available_to, "Date should be at least #{available_from.next.strftime("%d/%m/%Y")}")
end
end
How to use
h = Hotel.new
h.available_from = Date.parse("22/2/2014")
h.available_to = Date.parse("22/2/2014")
h.valid?
# => false
h.errors[:available_to]
# => ["Date should be at least 23/02/2014"]
However, when you're extending you validation rules, and code gets complicated, you should consider moving away validation from Model to separate Validation classes.
Check validates_with, with ActiveModel::Validator for more.
Hope that helps!

How to extract Mongoid documents based on a field value in the first or last embedded document?

I wish to find Order documents based on a field in the last embedded Notificationdocument.
In the example below I wish to find all pending orders that has one or more embedded notifications, and where the last notification has a datetime that is between 5 and 10 days old.
My suggestion here dosen't seem to do the trick...:
Order.where(status: 'pending').gte('notifications.last.datetime' => 5.days.ago).lte('notifications.last.datetime' => 10.days.ago)
Here are the two models:
class Order
include Mongoid::Document
field :datetime, type: DateTime
field :status, type: String, default: 'pending'
embeds_many :notifications, :inverse_of => :order
end
class Notification
include Mongoid::Document
field :datetime, type: DateTime
embedded_in :order, :inverse_of => :notifications
end
The main issue of the question seems to be how to refer to the LAST element of an array in the query.
Unfortunately, it is impossible as of MongoDB 2.4.
The simplest way to implement this feature is to use negative value to point to an element in an array like 'notifications.-1.datetime', but it doesn't work. (Refer to [#SERVER-5565] Handle negative array offsets consistently - MongoDB.)
To make matters worse, it also seems impossible to solve this using Aggregation Framework. There is no way to
add an array index to each element when $unwinding ([#SERVER-4588] aggregation: add option to $unwind to emit array index - MongoDB) or
select the index of an array dynamically when $projecting. ([#SERVER-4589] aggregation: need an array indexing operator - MongoDB)
Therefore, the only option you have seem to change the schema to match what you want. The simplest way is to add to Order one more field which contains datetime of the last Notification.
Update:
You can first get all candidates from the server, and then narrow down them on the client side to get the final result set. This involves no schema change. If the scale of database is relatively small or some degradation of performance is acceptable, this might be the best solution.
query = Order.where(status: 'pending').elem_match(
notifications: { datetime: { '$gte' => 10.days.ago, '$lte' => 5.days.ago } })
query.select do |order|
# datetime = order.notifications[0].datetime
datetime = order.notifications[order.notifications.size - 1].datetime
10.days.ago <= datetime && datetime <= 5.days.ago
end.each do |order|
p order # result
end
I know it comes a little late, but hey, better later than never. :P
You can use JavaScript in where:
Order.where("this.notifications[this.notifications.length - 1].datetime > new Date('#{5.days.ago}')")
Just found out that and was a huge relief having not to change my models. Hope that helps!

Resources