How to efficiently retry after failing Mongoid uniqueness validation? - ruby

I have a Mongoid model which requires two fields to be unique. I want to generate the "unique number" value and retry if it becomes not unique in the time it took to save.
class MyModel
include Mongoid::Document
include Mongoid::Timestamps
field :number
field :name
index({ number: 1 }, { unique: true, background: true })
index({ name: 1 }, { unique: true, background: true })
validates_presence_of :number, :name
validates_uniqueness_of :number, :name
def self.make!(name)
find_or_initialize_by(name: name).tap do |model|
if model.new_record?
model.number = generate_random_number
model.save! # fails validation because number is no longer unique
end
end
end
end
How can I retry when the uniqueness collision happens?

Begin
Rescue
Retry
Check
this example

Related

Why is validation being skipped?

I have an email ActiveRecord (sub-class, different PG DB) with the following validation:
class Email < DbRecord
belongs_to :user
attr_accessor :skip_validation
alias_method :skip_validation?, :skip_validation
validates :value,
presence: true,
uniqueness: true,
format: {
with: URI::MailTo::EMAIL_REGEXP,
message: "is an invalid email address",
allow_blank: true,
},
unless: :skip_validation?
before_validation { |record| record.value = record.value&.downcase }
skip_validation is nil. There are no other instance methods.
When there's no user_id the validation works as expected.
> e = Email.new(value: "foo#bar")
=> #<Email id: nil, user_id: nil, value: "foo#bar">
> e.valid?
=> false
When there's a user_id the bogus email doesn't trigger the validation.
> e = Email.new(user_id: 7, value: "foo#bar")
=> #<Email id: nil, user_id: 7, value: "foo#bar">
> e.valid?
=> true
Note that setting validate: true on belongs_to doesn't help:
class Email < DbRecord
belongs_to :user, validate: true
Still presents:
> e = Email.new(user_id: 7, value: "foo#bar")
=> #<Email id: nil, user_id: 7, value: "foo#bar">
> e.valid?
=> true
Why is that? What else should I be looking at/for?
Two-step answer:
"foo#bar" is a valid email according to URI::MailTo::EMAIL_REGEXP...
I can't even, but others have hit the same issue so...
As I always tell everyone else: check your assumptions. I assumed the validation was failing because of the email address, and in my sleep-deprived state I didn't verify the errors.
So why was it failing validation?
Rails 5 changed belongs_to to make the related ID mandatory, so in order for this to make sense (in my use-case) I also needed to add:
belongs_to :user, optional: true
to get the expected error messages back during validation.

Can't Use Validates Presence On Picture Rails 5.x

micropost.rb
class Micropost < ActiveRecord::Base
belongs_to :user
default_scope -> { order(created_at: :desc) }
mount_uploader :picture, PictureUploader
validates :user_id, presence: true
validates :tag1, presence: true, length: { maximum: 20 }
validates :tag2, presence: true, length: { maximum: 20 }
validates :tag3, presence: true, length: { maximum: 20 }
validates :picture, presence: true
validate :picture_size
validates :ispublic, inclusion: { in: [ true, false ] }
private
# Validates the size of an uploaded picture.
def picture_size
if picture.size > 5.megabytes
errors.add(:picture, "should be less than 5MB in size.")
end
end
end
microposts_controller.rb snippet:
def create
#micropost = current_user.microposts.build(micropost_params)
if #micropost.save!
flash[:success] = "Post created!"
redirect_to root_url
else
#feed_items = []
render 'static_pages/home'
end
end
def micropost_params
params.require(:micropost).permit(:tag1, :tag2, :tag3, :picture, :ispublic)
end
Test that is failing snippet:
tag1 = "This"
tag2 = "Is"
tag3 = "Sparta"
image = File.open("test/fixtures/p_avatar.png")
assert_difference 'Micropost.count', 1 do
post microposts_path, params: {micropost: { tag1: tag1, tag2: tag2, tag3: tag3, picture: "image",
ispublic: false }}
end
Test error:
Validation failed. Picture can't be blank.
I am using CarrierWave...based on other answers I was wondering if I can't use the presence: true validation with a picture, but I thought the #micropost.save would just store whatever as a string (varchar). For testing purposes, I just want to make sure a valid string is passed, but maybe that isn't even necessary if CarrierWave and picture_size validation is enough.
Found it. In the Rails spec, there is a function called fixture_file_upload() which does all the heavy lifting of creating the image object for me (some sort of upload to a cache it seems) and making the Model.save() method validate it and store the string in the db.
Thanks to this site it informed me a bit on image upload testing without going through FactoryBot: https://jeffkreeftmeijer.com/carrierwave-rails-test-fixtures/
Ultimately I decided I micropost doesn't require a picture (A la Twitter).

Mongo, Mongoid join data from different models in Ruby on Rails

I'm adapting a Ruby on Rails application to work with MongoDB instead of PostgreSQL. I have this 2 class:
module Cms
class Content
include Mongoid::Document
include Mongoid::Orderable
field :title, type: String
field :position, type: String
field :text_body, type: String
field :expiration_date, type: Date
field :active, type: Boolean, default: false
validates :title, :text_body, presence: true
orderable base: 0
belongs_to :cms_content_category, class_name: 'Cms::ContentCategory'
scope :ordered, ->() { order(position: :asc) }
scope :not_expired, ->() { any_of({ expiration_date: nil }, { :expiration_date.gte => Date.today }) }
end
end
module Cms
class Submenu
include Mongoid::Document
include Mongoid::Orderable
field :title, type: String
field :position, type: String
field :external_url, type: String
field :open_as, type: String, default: '_self'
field :active, type: Boolean, default: false
delegate :url_helpers, to: :'Rails.application.routes'
validates :title, presence: true
validates :external_url, presence: true, if: Proc.new { |obj| obj.cms_content_id.nil? }
validates :cms_content_id, presence: true, if: Proc.new { |obj| obj.external_url.blank? }
orderable base: 0
belongs_to :cms_content, class_name: 'Cms::Content'
belongs_to :cms_menu, class_name: 'Cms::Menu'
scope :ordered, ->() { order(position: :asc) }
def url
self.external_url.present? ? self.external_url : url_helpers.content_show_path(self.cms_content_id)
end
end
end
The problem is that I don't know how to adapt the following SQL query to work with MongoDB.
scope :active, joins("FULL JOIN contents ON contents.id = cms_submenus.content_id")
.where("cms_submenus.active IS TRUE AND (
(cms_submenus.content_id IS NOT NULL AND ((contents.expiration_date >= :today OR expiration_date IS NULL) AND contents.archived IS NOT TRUE)) OR
(cms_submenus.external_url IS NOT NULL AND cms_submenus.external_url <> '')
)", today: Date.today)
This scope has to be called from Submenu model.
Someone has an idea on how to solve this problem?
I'm using Rails 4.2.0 with Ruby 2.2.0 and Mongoid 4.0.2 with MongoDB 2.6.8.
thank you

Unable to create correct Mongoid model from JSON

I'm trying to create a Mongoid model from the corresponding JSON structure.
However it does not include the embedded relation frameworks.
I'm using Mongoid 4.0. Am I doing something wrong or is this a bug?
If I store any embedded relation via store_as under a different name than the default serialization, it works as expected. Also, if I create the model in the database from JSON rather than initialize it everything's fine...
JSON input
{
"name": "MyName",
"frameworks": [
{
"name": "grails",
"runtime": "groovy",
"versions": []
}
]
}
Models
require 'mongoid'
class Vendor
include Mongoid::Document
include Mongoid::Attributes::Dynamic
# fields
field :name, type: String
# relations
embeds_many :frameworks
# validations
validates :name, presence: true
validates :frameworks, presence: true
end
class Framework
include Mongoid::Document
embedded_in :vendor
field :name, type: String
field :runtime, type: String
field :versions, type: Array
# validations
validates :name, presence: true
validates :runtime, presence: true
end
Test App
require 'json'
require 'require_relative'
require_relative 'vendor'
begin
json = JSON.parse(File.read('input.json'))
#profile = Vendor.new(json)
puts #profile.inspect
rescue JSON::ParserError => e
puts "Error: " << e.to_s
end

Rails ActiveRecord Ignoring Params and Saving Nil Data

This is stumping me; For some reason the db is saving the record with nil fields instead of my params. Can anyone help me understand why ActiveRecord isn't using my params?
db migration:
class CreateRoutes < ActiveRecord::Migration
def change
create_table :routes do |t|
t.integer :user_id
t.string :start_address
t.string :end_address
t.text :waypoints
t.text :schedule
t.integer :duration
t.timestamps
end
add_index :routes, :user_id
end
end
route.rb:
class Route < ActiveRecord::Base
attr_accessor :start_address, :end_address, :waypoints, :schedule, :duration
belongs_to :user
#serialize :waypoints, :schedule
validates :user_id, presence: true
validates :start_address, presence: true
validates :end_address, presence: true
validates :schedule, presence: true
validates :duration, presence: true, numericality: { only_integer: true, greater_than: 0 }
end
routes_controller.rb:
class RoutesController < ApplicationController
.
.
.
def create
logger.debug "\n\n*** #{params[:route]} ***"
#route = current_user.routes.build(params[:route])
logger.debug "*** The route is #{#route.inspect} ***\n\n"
if #route.save
flash[:success] = "Route saved!"
redirect_to user_path(current_user)
else
render 'new'
end
end
.
.
.
end
logger output:
*** {"start_address"=>"123 Sample St.", "end_address"=>"321 Elpmas St.", "waypoints"=>"None", "schedule"=>"Mondays", "duration"=>"15"} ***
*** The route is #<Route id: nil, user_id: 1, start_address: nil, end_address: nil, waypoints: nil, schedule: nil, duration: nil, created_at: nil, updated_at: nil> ***
The attr_accessors will overwrite the accessors generated by ActiveRecord, causing them to not be persisted in the DB--they'll be like plain old Ruby properties/members instead of the meta-programmed magic of ActiveRecord.
DB properties (persistent properties) can have things like attr_accessible, though.

Resources