Verify uniqueness of single value across multiple columns - ruby-on-rails

I have an ActiveRecord object that has four String columns. I'd like to make a validation that verifies that a certain value is unique across all four columns. For example, assuming the four columns in question are named a, b, c, and d:
FooObject.new( a: 'bar' ).save!
should succeed, but
FooObject.new( b: 'bar' ).save!
should fail because there is already a FooObject whose value of either a, b, c, or d matches the value entered for b. Is there a neat, clean way to accomplish this validation on the object? Thank you!

arr = ["a", "b"]
arr.uniq{|x| x}.size //2
arr << "b"
arr.uniq{|x| x}.size // 2
2 == 2
hence this element already exists in the column
arr represents a temp copy of one row of FoObject

You can try a custom method:
validate :uniqueness_across_columns
def uniqueness_across_columns
cols = [:a, :b, :c, :d]
conditions = cols.flat_map{|x| cols.map{|i| arel_table[x].eq(self.try(i)) if self.try(i).present? }}
!self.class.exists? conditions.compact.reduce(:or)
end

Related

Forcing a particular order in Rails model scope

I have a Model M in my rails code. It has a field F which can have 4 values D, J, M and Z
If I use a scope like this, it would sort data by field F alphabetically:
default scope {order (F: :asc)}
I have 2 questions here:
I don't want to sort data alphabetically on F. I would like data to be displayed with this particular order of F instead. I want ALWAYS the records containing value M for field F first, follow by records having value J, D and then Z in this order. How can I achieve this?
Suppose I would like to display records having J first and then sort the rest of the records by alphabetical order of field F, how can I do that?
You can sort with a CASE statement
order("CASE WHEN F = 'M' THEN 0 WHEN F = 'J' THEN 1 WHEN F = 'D' THEN 2 ELSE 3 END")
Alternatively (if it's only "M" that needs to be first and the rest can be alphabetical)
order("CASE WHEN F = 'M' THEN 0 ELSE 1 END, F")

Ruby on Rails - select where ALL ids in array

I'm trying to find the cleanest way to select records based on its associations and a search array.
I have Recipes which have many Ingredients (through a join table)
I have a search form field for an array of Ingredient.ids
To find any recipe which contains any of the ids in the search array, I can use
eg 1.
filtered_meals = Recipe.includes(:ingredients).where("ingredients.id" => ids)
BUT, I want to only match recipes where ALL of it's ingredients are found in the search array.
eg 2.
search_array = [1, 2, 3, 4, 5]
Recipe1 = [1, 4, 5, 6]
Recipe2 = [1, 3, 4]
# results => Recipe2
I am aware that I can use an each loop, something like this;
eg 3.
filtered_meals = []
Recipes.each do |meal|
meal_array = meal.ingredients.ids
variable = meal_array-search_array
if variable.empty?
filtered_meals.push(meal)
end
end
end
return filtered_meals
The problem here is pagination. In the first example I can use .limit() and .offset() to control how many results are shown, but in the third example I would need to add an extra counter, submit that with the results, and then on a page change, re-send the counter and use .drop(counter) on the each.do loop.
This seems way too long winded, is there any better way to do this??
Assuming you are using has_many through & recipe_id, ingredient_id combination are unique.
recipe_ids = RecipeIngredient.select(:recipe_id)
.where(ingredient_id: ids)
.group(:recipe_id)
.having("COUNT(*) >= ?", ids.length)
filtered_meals = Recipe.find recipe_ids
How about
filtered_meals = Recipe.joins(:ingredients)
.group(:recipe_id)
.order("ingredients.id ASC")
.having("array_agg(ingredients.id) = ?", ids)
You'll need to make sure your ids parameter is listed in ascending order so the order of the elements in the arrays will match too.
Ruby on Rails Guide 2.3.3 - Subset Conditions
Recipe.all(:ingredients => { :id => search_array })
Should result in:
SELECT * FROM recipes WHERE (recipes.ingredients IN (1,2,3,4,5))
in SQL.
Would the array & operator work for you here?
Something like:
search_array = [1, 2, 3, 4, 5]
recipe_1 = [1, 4, 5, 6]
recipe_2 = [1, 3, 4]
def contains_all_ingredients?(search_array, recipe)
(search_array & recipe).sort == recipe.sort
end
contains_all_ingredients(search_array, recipe_1) #=> false
contains_all_ingredients(search_array, recipe_2) #=> true
This method compares the arrays and returns only the elements present in both, so if the result of the comparison equals the recipe array, all are present. (And obviously you could have a little refactor to have the method sit in the recipe model.)
You could then do:
Recipes.all.select { |recipe| contains_all_ingredients?(search_array, recipe) }
I'm not sure it passes your example three, but might help on your way? Let me know if that starts off OK, and I'll have more of a think in the meantime / if it's useful :)
I had a similar need and solved it using the pattern below. This is what the method looks like in my Recipe model.
def self.user_has_all_ingredients(ingredient_ids)
# casts ingredient_ids to postgres array syntax
ingredient_ids = '{' + ingredient_ids.join(', ') + '}'
return Recipe.joins(:ingredients)
.group(:id)
.having('array_agg(ingredients.id) <# ?', ingredient_ids)
end
This returns every recipe where all of the required ingredients are included in an ingredients array.
The Postgres '<#' operator was the magic solution. The array_agg function creates an array of each recipe's ingredient ids and then the left-pointing bird operator asks whether all of the unique ids in that array are contained in the array on the right.
Using the array_agg function required me to cast my search_array into Postgres syntax.
My Recipes model has many Ingredients through Portions.
I'd love to know if anyone has any better optimizations or knows how to avoid the casting to Postgres syntax that I needed to do.

Return values were not extracted in Ruby

def get_dept_class_type
departments, classifications, types = [["-- select one --"]] * 3
Department.all.each do |d|
departments << d.name
end
Classification.all.each do |c|
classifications << c.name
end
Type.all.each do |t|
types << t.name
end
return departments, classifications, types
end
def new
#departments, #classifications, #types = get_dept_class_type
end
Hello guys,
above is my code in Ruby to assign the return values from "get_dept_class_type" function to "def new" instance variables. The problem is, the return values from the "get_dept_class_type" function were not extracted, So all instance variables have the same values. Each instance variable is the containing value of a select tag in html form.
The values of department, classification, type select tags have these the same content:
Information Technology
Administrative Department
Internal Use
Top Secret
Confidential
Public
Policy
Procedure Manual
Please it help me to figure this out. Thank you in advanced.
Your main problem is -
departments, classifications, types = [["-- select one --"]] * 3
changed it to -
departments, classifications, types = Array.new(3) { ["-- select one --"] }
Lets debug it :-
([[]] * 3).map(&:object_id) # => [72506330, 72506330, 72506330]
but
Array.new(3) { [] }.map(&:object_id) # => [76642680, 76642670, 76642520]
You can see, that all the inner objects are basically same object. Basically you have created an array of array, say a, where all the element arrays are same object. Thus if you have modified, say a[0], you can see the same change when you would inspect a[1] or a[2]. But if you do create the same array of array, a as , Array.new(3) { [] }, then each inner element array of a, will be different object. Thus if you say modify a[0], then a[1] and a[2] will be intact.
Worth to read Common gotchas.

Best way to order records by predetermined list

In order to keep things simple I have avoided using an enum for an attribute, and am instead storing string values.
I have a list of all possible values in a predetermined order in the array: MyTypeEnum.names
And I have an ActiveRecord::Relation list of records in my_recs = MyModel.order(:my_type)
What is the best way to order records in my_recs by their :my_type attribute value in the order specified by the array of values in MyTypeEnum.names ?
I could try this:
my_recs = MyModel.order(:my_type)
ordered_recs = []
MyTypeEnum.names.each do |my_type_ordered|
ordered_recs << my_recs.where(:my_type => my_type_ordered)
end
But am I killing performance by building an array instead of using the ActiveRecord::Relation? Is there a cleaner way? (Note: I may want to have flexibility in the ordering so I don't want to assume the order is hardcoded as above by the order of MyTypeEnum.names)
You are definitely taking a performance hit by doing a separate query for every item in MyTypeEnum. This requires only one query (grabbing all records at once).
ordered_recs = Hash[MyTypeEnum.names.map { |v| [v, []] }]
MyModel.order(:my_type).all.each do |rec|
ordered_recs[rec.my_type] << rec
end
ordered_recs = ordered_recs.values.flatten
If MyTypeEnum contains :a, :b, and :c, ordered_recs is initialized with a Hash of Arrays keyed by each of the above symbols
irb(main):002:0> Hash[MyTypeEnum.names.map { |v| [v, []] }]
=> {:a=>[], :b=>[], :c=>[]}
The records are appended to the proper Array based on it's key in the Hash, and then when all have bene properly grouped, the arrays are concatenated/flattened together into a single list of records.

Array manipulation (summing one column by certain group, keeping others the same)

I have a table of Logs with the columns name, duration, type, ref_id.
I update the table every so often so perhaps it will look like a col of ['bill', 'bob', 'bob', 'jill'] for names, and [3, 5, 6, 2] for duration, and ['man', boy', 'boy', 'girl'] for type, and [1, 2, 2, 3] for ref_id.
I would like to manipulate my table so that I can add all the durations so that I get a hash or something that looks like this:
{'name' => ['bill', 'bob', 'jill'], 'duration' => [3, 11, 2], 'type' => ['man', 'boy', 'girl'], ref_id => [1, 2, 3]}
How can I do this?
(for more info--currently I'm doing Log.sum(:duration, :group => 'name') which gives me the names themselves as the keys (bill, bob, jill) instead of the column name, with the correct duration sums as their values (3, 11, 2). but then I lose the rest of the data...)
I guess I could manually go through each log and add the name/type/ref_id if it's not in the hash, then add onto the duration. If so what's the best way to do that?
If you know of good sources on rails array manipulation/commonly used idioms, that would be great too!
Couple of notes first.
Your table is not properly normalized. You should split this table into (at least) two: users, and durations. You should do this for lots of reasons, that's relational databases 101.
Also, the hash you want as a result also doesn't look right, it suggests that you are pre-grouping data to suit your presentation. It's usually more logical to put these results in an array of hashes, than in a hash of arrays.
Now on to the answer:
With your table, you can simply do GROUP BY:
SELECT name, type, ref_id, SUM(duration) as duration
FROM logs
GROUP BY name, type, ref_id
or, using AR:
durations = Log.find(:all,
:select => 'name, type, ref_id, SUM(duration) as duration',
:group => 'name, type, ref_id'
)
In order to convert this to a hash of arrays, you'd use something like:
Hash[
%w{name, type, ref_id, duration}.map{|f|
[f, durations.map{|h|
h.attributes[f]
}]
}
]
Maybe all you need is something like this that spins through all the log entries and collects the results:
# Define attributes we're interested in
operate_on = %w[ name duration type ref_id ]
# Create a new hash with placeholder hashes to collect instances
summary = Hash[operate_on.map { |k| [ k, { } ] }]
Log.all.collect do |log|
operate_on.each do |attr|
# Flag this attribute/value pair as having been seen
summary[attr][log.send(attr)] = true
end
end
# Extract only the keys, return these as a hash
summary = Hash[summary.map { |key, value| [ key, value.keys ] }]
A more efficient method would be to do this as several SELECT DISTINCT(x) calls instead of instancing so many models.
Didn't quite understand if you want to save records from your hash, or you want to query the table and get results back in this form. If you want to get a hash back, then this should work:
Log.all.inject({}) do |hash, l|
hash['name'] ||= []
hash['duration'] ||= []
hash['type'] ||= []
hash['ref_id'] ||= []
hash['name'] << l.name
hash['duration'] << l.duration
hash['type'] << l.type
hash['ref_id'] << l.ref_id
hash
end

Resources