Store functions in mongodb using Mongoid 3 - ruby-on-rails

Just as the title suggests. I am not able to find anything related to Mongoid 3. The things I found only apply to old versions of mongoid that didn't use Moped.
I found this and it doesn't work:
def self.install_javascript
getWeekJs = Rails.root.join("lib/javascript/getWeek.js")
if collection.master['system.js'].find_one({'_id' => "getWeek"}).nil?
collection.master.db.add_stored_function("getWeek", File.new(getWeekJs).read)
end
end
This method would add a getWeek function to the system.js collection.
How can this be done in mongoid 3?

Nailed it!
Codes:
class StoredProcedure
include Mongoid::Document
store_in collection: "system.js"
field :_id, type: String, default: ""
def self.test
equalsJS = Rails.root.join("lib/assets/javascripts/equals.js")
code = Moped::BSON::Code.new(File.new(equalsJS).read)
unless where(id: "equals").first
proc = new(value: code)
proc._id = "equals"
proc.save
end
end
end
Explanation:
I'm using the system.js in mongoid as if it were a normal collection. I'm then simply adding new documents.
IMPORTANT:
The value needs to be a Moped::BSON::Code instance otherwise it will be saved as string, thus useless. The id needs to be the function's name. I wasn't able to specify the id in a create statement, therefore I added multiple steps.
Just add this to a rake task to make sure you add all required functions to mongo after deployment.

Related

Use Enumerators in Rails 7 with MongoDB

I'm building a new project with Rails 7 and MongoDB 8. And I wanted to use enumerators for multiple fields ( states etc .. )
I wanted to use the gem mongoid-enum but it doesn't work with Mongo 8.
Is switching to SQL database a solution ? Or is there any other way ?
I've checked on Mongo's doc and found a Phantom Custom Field Types but it looks like it's not saving in the db. In the rails console, I'll do the Model.status = "open" then saving it, it doesn't return any errors. So I close the console then open it again. Run the Model.status and it returns nil.
Thank you for reading and trying to help me !
First of all, there is good and bad in both MongoDB and PostgreSQL, it depends of the kind of features you need, see: https://www.geeksforgeeks.org/difference-between-postgresql-and-mongodb/
About the Phantom Custom Field Types, this is indeed doing the same stuff than ActiveRecord::Enum but asking more code to write. Could you share the not working code that you've run for your test please ?
Edit 07/02/22:
Here is an example of you could use enum in mongo without writing too much code:
module MongoEnum
# Takes application-scope value and converts it to how it would be
# stored in the database. Converts invalid values to nil.
def mongoize(object)
mapping[object]
end
# Get the value as it was stored in the database, and convert to
# application-scope value. Converts invalid values to nil.
def demongoize(object)
inverse_mapping[object]
end
# Converts the object that was supplied to a criteria and converts it
# into a query-friendly form. Returns invalid values as is.
def evolve(object)
mapping.fetch(object, object)
end
def mapping
#mapping ||= self.const_get(:MAPPING).freeze
end
def inverse_mapping
#inverse_mapping ||= mapping.invert.freeze
end
end
class RoleEnum
extend MongoEnum
MAPPING = {
'admin' => 0,
'user' => 1,
}.freeze
end
class ColorEnum
extend MongoEnum
MAPPING = {
'black' => 0,
'white' => 1,
}.freeze
end
class Profile
include Mongoid::Document
field :color, type: ColorEnum
end
class User
include Mongoid::Document
field :role, type: RoleEnum
end
Disclaimer: I didn't test it in a real app, let me know if it does not work.

Ruby on rails: How to create single api for create and update both actions [duplicate]

I have a class called CachedObject that stores generic serialized objects indexed by a key. I want this class to implement a create_or_update method. If an object is found it will update it, otherwise it will create a new one.
Is there a way to do this in Rails or do I have to write my own method?
Rails 6
Rails 6 added an upsert and upsert_all methods that deliver this functionality.
Model.upsert(column_name: value)
[upsert] It does not instantiate any models nor does it trigger Active Record callbacks or validations.
Rails 5, 4, and 3
Not if you are looking for an "upsert" (where the database executes an update or an insert statement in the same operation) type of statement. Out of the box, Rails and ActiveRecord have no such feature. You can use the upsert gem, however.
Otherwise, you can use: find_or_initialize_by or find_or_create_by, which offer similar functionality, albeit at the cost of an additional database hit, which, in most cases, is hardly an issue at all. So unless you have serious performance concerns, I would not use the gem.
For example, if no user is found with the name "Roger", a new user instance is instantiated with its name set to "Roger".
user = User.where(name: "Roger").first_or_initialize
user.email = "email#example.com"
user.save
Alternatively, you can use find_or_initialize_by.
user = User.find_or_initialize_by(name: "Roger")
In Rails 3.
user = User.find_or_initialize_by_name("Roger")
user.email = "email#example.com"
user.save
You can use a block, but the block only runs if the record is new.
User.where(name: "Roger").first_or_initialize do |user|
# this won't run if a user with name "Roger" is found
user.save
end
User.find_or_initialize_by(name: "Roger") do |user|
# this also won't run if a user with name "Roger" is found
user.save
end
If you want to use a block regardless of the record's persistence, use tap on the result:
User.where(name: "Roger").first_or_initialize.tap do |user|
user.email = "email#example.com"
user.save
end
In Rails 4 you can add to a specific model:
def self.update_or_create(attributes)
assign_or_new(attributes).save
end
def self.assign_or_new(attributes)
obj = first || new
obj.assign_attributes(attributes)
obj
end
and use it like
User.where(email: "a#b.com").update_or_create(name: "Mr A Bbb")
Or if you'd prefer to add these methods to all models put in an initializer:
module ActiveRecordExtras
module Relation
extend ActiveSupport::Concern
module ClassMethods
def update_or_create(attributes)
assign_or_new(attributes).save
end
def update_or_create!(attributes)
assign_or_new(attributes).save!
end
def assign_or_new(attributes)
obj = first || new
obj.assign_attributes(attributes)
obj
end
end
end
end
ActiveRecord::Base.send :include, ActiveRecordExtras::Relation
The magic you have been looking for has been added in Rails 6
Now you can upsert (update or insert).
For single record use:
Model.upsert(column_name: value)
For multiple records use upsert_all :
Model.upsert_all(column_name: value, unique_by: :column_name)
Note:
Both methods do not trigger Active Record callbacks or validations
unique_by => PostgreSQL and SQLite only
Add this to your model:
def self.update_or_create_by(args, attributes)
obj = self.find_or_create_by(args)
obj.update(attributes)
return obj
end
With that, you can:
User.update_or_create_by({name: 'Joe'}, attributes)
Old question but throwing my solution into the ring for completeness.
I needed this when I needed a specific find but a different create if it doesn't exist.
def self.find_by_or_create_with(args, attributes) # READ CAREFULLY! args for finding, attributes for creating!
obj = self.find_or_initialize_by(args)
return obj if obj.persisted?
return obj if obj.update_attributes(attributes)
end
By chaining find_or_initialize_by and update, this can be achieved in a simple way which avoids the (in my experience, often) unwanted caveats of upsert, and also minimises database calls.
For example:
Class.find_or_initialize_by(
key: "foo",
...
).update(
new_attribute: "bar",
...
)
will return you newly created or updated object.
It is worth noting that if your find_or_initialize_by attributes match multiple Class instances, only the 'first' one will be selected and updated.
You can do it in one statement like this:
CachedObject.where(key: "the given key").first_or_create! do |cached|
cached.attribute1 = 'attribute value'
cached.attribute2 = 'attribute value'
end
The sequel gem adds an update_or_create method which seems to do what you're looking for.

Existing data serialized as hash produces error when upgrading to Rails 5

I am currently upgrading a Ruby on Rails app from 4.2 to 5.0 and am running into a roadblock concerning fields that store data as a serialized hash. For instance, I have
class Club
serialize :social_media, Hash
end
When creating new clubs and inputting the social media everything works fine, but for the existing social media data I'm getting:
ActiveRecord::SerializationTypeMismatch: Attribute was supposed to be a Hash, but was a ActionController::Parameters.
How can I convert all of the existing data from ActionController::Parameter objects to simple hashes? Database is mysql.
From the fine manual:
serialize(attr_name, class_name_or_coder = Object)
[...] If class_name is specified, the serialized object must be of that class on assignment and retrieval. Otherwise SerializationTypeMismatch will be raised.
So when you say this:
serialize :social_media, Hash
ActiveRecord will require the unserialized social_media to be a Hash. However, as noted by vnbrs, ActionController::Parameters no longer subclasses Hash like it used to and you have a table full of serialized ActionController::Parameters instances. If you look at the raw YAML data in your social_media column, you'll see a bunch of strings like:
--- !ruby/object:ActionController::Parameters...
rather than Hashes like this:
---\n:key: value...
You should fix up all your existing data to have YAMLized Hashes in social_media rather than ActionController::Parameters and whatever else is in there. This process will be somewhat unpleasant:
Pull each social_media out of the table as a string.
Unpack that YAML string into a Ruby object: obj = YAML.load(str).
Convert that object to a Hash: h = obj.to_unsafe_h.
Write that Hash back to a YAML string: str = h.to_yaml.
Put that string back into the database to replace the old one from (1).
Note the to_unsafe_h call in (3). Just calling to_h (or to_hash for that matter) on an ActionController::Parameters instance will give you an exception in Rails5, you have to include a permit call to filter the parameters first:
h = params.to_h # Exception!
h = params.permit(:whatever).to_h # Indifferent access hash with one entry
If you use to_unsafe_h (or to_unsafe_hash) then you get the whole thing in a HashWithIndifferentAccess. Of course, if you really want a plain old Hash then you'd say:
h = obj.to_unsafe_h.to_h
to unwrap the indifferent access wrapper as well. This also assumes that you only have ActionController::Parameters in social_media so you might need to include an obj.respond_to?(:to_unsafe_hash) check to see how you unpack your social_media values.
You could do the above data migration through direct database access in a Rails migration. This could be really cumbersome depending on how nice the low level MySQL interface is. Alternatively, you could create a simplified model class in your migration, something sort of like this:
class YourMigration < ...
class ModelHack < ApplicationRecord
self.table_name = 'clubs'
serialize :social_media
end
def up
ModelHack.all.each do |m|
# Update this to match your real data and what you want `h` to be.
h = m.social_media.to_unsafe_h.to_h
m.social_media = h
m.save!
end
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
You'd want to use find_in_batches or in_batches_of instead all if you have a lot of Clubs of course.
If your MySQL supports json columns and ActiveRecord works with MySQL's json columns (sorry, PostgreSQL guy here), then this might be a good time to change the column to json and run far away from serialize.
Extending on short's reply - a solution that does not require a database migration:
class Serializer
def self.load(value)
obj = YAML.load(value || "{}")
if obj.respond_to?(:to_unsafe_h)
obj.to_unsafe_h
else
obj
end
end
def self.dump(value)
value = if value.respond_to?(:to_unsafe_h)
value.to_unsafe_h
else
value
end
YAML.dump(value)
end
end
serialize :social_media, Serializer
Now club.social_media will work whether it was created on Rails 4 or on Rails 5.
The reply by #schor was a life-saver, but I kept getting no implicit conversion of nil into String errors when doing the YAML.load(value).
What worked for me was:
class Foo < ApplicationRecord
class NewSerializer
def self.load(value)
return {} if !value #### THIS NEW LINE
obj = YAML.load(value)
if obj.respond_to?(:to_unsafe_h)
obj.to_unsafe_h
else
obj
end
end
def self.dump(value)
if value.respond_to?(:to_unsafe_h)
YAML.dump(value.to_unsafe_h)
else
YAML.dump(value)
end
end
end
serialize :some_hash_field, NewSerializer
end
I gotta admin the Rails team totally blindsided me on this one, a most unwelcome breaking change that doesn't even let an app fetch the "old" data.
The official Ruby on Rails documentation has a section about upgrading between Rails versions that explains more about the error you have:
ActionController::Parameters No Longer Inherits from HashWithIndifferentAccess
Calling params in your application will now return an object instead of a hash. If your parameters are already permitted, then you will not need to make any changes. If you are regardless of permitted? you will need to upgrade your application to first permit and then convert to a hash.
params.permit([:proceed_to, :return_to]).to_h
Run a migration on Rails 4 to prepare the data for Rails 5.
We're going through the exact same thing, except we serialize as ActiveSupport::HashWithIndifferentAccess instead of just Hash, which I recommend doing, but I'll provide my answer here for just a simple Hash.
If you have not yet upgraded to Rails 5, which I hope you haven't and your tests have uncovered this issue, you can run a migration on the Rails 4 branch that will get your data ready for Rails 5.
It essentially re-serializes all of your records from ActionController::Parameters to Hash while in Rails 4 and ActionController::Parameters still inherits from HashWithIndifferentAccess.
class ConvertSerializedActionControllerParametersToHashInClubs < ActiveRecord::Migration
disable_ddl_transaction! # This prevents the locking of the table (e.g. in production).
def up
clubs = Club.where.not( social_media: nil )
total_records = clubs.count
say "Updating #{ total_records } records."
clubs.each.with_index( 1 ) do |club, index|
say "Updating #{ index } of #{ total_records }...", true
club.social_media = club.social_media.to_h
club.social_media_will_change!
club.save
end
end
def down
puts "Cannot be reverse! See backup table."
end
end
If you have multiple columns that need to be converted, it's easy to modify this migration to convert all of the necessary tables and columns.
Depending on when you do this, your data should be ready for Rails 5.

Rails; Fetch records within initializer

I've been wondering it is common to fetch records within initializer?
Here this is an example for service object to fetch records and generated pdf receipt file.
Input is invoice uuid, and fetch the related records such as card detail, invoice items within initialier.
class Pdf::GenerateReceipt
include Service
attr_reader :invoice, :items, :card_detail
def initialize(invoice_uuid)
#invoice ||= find_invoice!(invoice_uuid) # caching
#items = invoice.invoice_items
#card_detail = card_detail
end
.....
def call
return ReceiptGenerator.new(
id: invoice.uuid, # required
outline: outline, # required
line_items: line_items, # required
customer_info: customer_info
)
rescue => e
false, e
end
.....
def card_detail
card_metadata = Account.find(user_detail[:id]).credit_cards.primary.last
card_detail = {}
card_detail[:number] = card_metadata.blurred_number
card_detail[:brand] = card_metadata.brand
card_detail
end
end
Pdf::GenerateReceipt.('28ed7bb1-4a3f-4180-89a3-51cb3e621491') # => then generate pdf
The problem is if the records not found, this generate an error.
I could rescue within the initializer, however that seems not common.
How could I work around this in more ruby way?
This is mostly opinion and anecdotal, but I prefer to deal with casting my values as far up the chain as possible. So i would find the invoice before this object and pass it in as an argument, same with the card_detail.
If you do that in this class, it will limit the responsibility to coordinating those two objects, which is way easier to test but also adds another layer that you have to reason about in the future.
So how i would handle, split this into 4 separate things
Invoice Finder thing
Card Finder thing
Pdf Generator that takes invoice and card as arguments
Finally, something to orchestrate the 3 actions above
Hope this helps.
Addition: Check out the book confident ruby by avdi grimm. It's really great for outlining handling this type of scenario.

Rails create or update magic?

I have a class called CachedObject that stores generic serialized objects indexed by a key. I want this class to implement a create_or_update method. If an object is found it will update it, otherwise it will create a new one.
Is there a way to do this in Rails or do I have to write my own method?
Rails 6
Rails 6 added an upsert and upsert_all methods that deliver this functionality.
Model.upsert(column_name: value)
[upsert] It does not instantiate any models nor does it trigger Active Record callbacks or validations.
Rails 5, 4, and 3
Not if you are looking for an "upsert" (where the database executes an update or an insert statement in the same operation) type of statement. Out of the box, Rails and ActiveRecord have no such feature. You can use the upsert gem, however.
Otherwise, you can use: find_or_initialize_by or find_or_create_by, which offer similar functionality, albeit at the cost of an additional database hit, which, in most cases, is hardly an issue at all. So unless you have serious performance concerns, I would not use the gem.
For example, if no user is found with the name "Roger", a new user instance is instantiated with its name set to "Roger".
user = User.where(name: "Roger").first_or_initialize
user.email = "email#example.com"
user.save
Alternatively, you can use find_or_initialize_by.
user = User.find_or_initialize_by(name: "Roger")
In Rails 3.
user = User.find_or_initialize_by_name("Roger")
user.email = "email#example.com"
user.save
You can use a block, but the block only runs if the record is new.
User.where(name: "Roger").first_or_initialize do |user|
# this won't run if a user with name "Roger" is found
user.save
end
User.find_or_initialize_by(name: "Roger") do |user|
# this also won't run if a user with name "Roger" is found
user.save
end
If you want to use a block regardless of the record's persistence, use tap on the result:
User.where(name: "Roger").first_or_initialize.tap do |user|
user.email = "email#example.com"
user.save
end
In Rails 4 you can add to a specific model:
def self.update_or_create(attributes)
assign_or_new(attributes).save
end
def self.assign_or_new(attributes)
obj = first || new
obj.assign_attributes(attributes)
obj
end
and use it like
User.where(email: "a#b.com").update_or_create(name: "Mr A Bbb")
Or if you'd prefer to add these methods to all models put in an initializer:
module ActiveRecordExtras
module Relation
extend ActiveSupport::Concern
module ClassMethods
def update_or_create(attributes)
assign_or_new(attributes).save
end
def update_or_create!(attributes)
assign_or_new(attributes).save!
end
def assign_or_new(attributes)
obj = first || new
obj.assign_attributes(attributes)
obj
end
end
end
end
ActiveRecord::Base.send :include, ActiveRecordExtras::Relation
The magic you have been looking for has been added in Rails 6
Now you can upsert (update or insert).
For single record use:
Model.upsert(column_name: value)
For multiple records use upsert_all :
Model.upsert_all(column_name: value, unique_by: :column_name)
Note:
Both methods do not trigger Active Record callbacks or validations
unique_by => PostgreSQL and SQLite only
Add this to your model:
def self.update_or_create_by(args, attributes)
obj = self.find_or_create_by(args)
obj.update(attributes)
return obj
end
With that, you can:
User.update_or_create_by({name: 'Joe'}, attributes)
Old question but throwing my solution into the ring for completeness.
I needed this when I needed a specific find but a different create if it doesn't exist.
def self.find_by_or_create_with(args, attributes) # READ CAREFULLY! args for finding, attributes for creating!
obj = self.find_or_initialize_by(args)
return obj if obj.persisted?
return obj if obj.update_attributes(attributes)
end
By chaining find_or_initialize_by and update, this can be achieved in a simple way which avoids the (in my experience, often) unwanted caveats of upsert, and also minimises database calls.
For example:
Class.find_or_initialize_by(
key: "foo",
...
).update(
new_attribute: "bar",
...
)
will return you newly created or updated object.
It is worth noting that if your find_or_initialize_by attributes match multiple Class instances, only the 'first' one will be selected and updated.
You can do it in one statement like this:
CachedObject.where(key: "the given key").first_or_create! do |cached|
cached.attribute1 = 'attribute value'
cached.attribute2 = 'attribute value'
end
The sequel gem adds an update_or_create method which seems to do what you're looking for.

Resources