Rails Store Array in Database - sql

I need to persist an array in my rails language learning app. Here is the situation/reason I need to do this (if there is a better way please let me know). The user is presented, one at a time, 10 words to learn. After the words are learned they get quized on the same set of 10 words.
For the 'quiz' portion I would like to randomize the order the words appear in (for example: currently if you learn the words 1.fish 2.cat 3.dog... you will be quized in the same order 1.fish 2.cat 3.dog... which can make the quiz easier.
I need to persist it in case the user were to log off or navigate away. In this instance I want to return them to the exact word they left off on the quiz.
Here is the controller code I currently have:
def index
.
.
#quiz = Lang.limit(10).offset(current_user.bookmark - 11)
exercise_bank
.
.
end
private
def exercise_bank
current_user.random_exercise_array = (0..9).step(1).shuffle
current_user.save
#index2 = current_user.random_exercise_array[#index]
##index starts at 0 and increments on each call to index action
##index2 grabs the random number and uses it to reference a word in #quiz
#something like this: #quiz[#index2].spanish_to_english---grabs a english word
end
end
The idea of the above is to pick a random number 0-9 and use it to reference a word in my DB. The above results in something like the following in my DB for the random_exercise_array attribute:
random_exercise_array: "---\n- 6\n- 0\n- 1\n- 7\n- 8\n- 5\n- 9\n- 3\n- 2\n- 4\n"
Here is the relevant User DB code:
class DeviseCreateUsers < ActiveRecord::Migration
serialize :random_exercise_array
end
Relevant migration file:
class AddRandomExerciseArrayToUsers < ActiveRecord::Migration
def change
add_column :users, :random_exercise_array, :text
end
end
Is this the best way to solve this problem? If so, can you explain how to get back an integer from random_exercise_array without all of the (-)'s and (\n')s?

If you want get an array back from DB text column, you should declare the type in your user model like this:
class DeviseCreateUsers < ActiveRecord::Base #why your original code inherit from ActiveRecord::Migration?
serialize :random_exercise_array, Array
end
Then you can use current_user.random_exercise_array as an array instead of a string.

Related

Rails 5 - using synthetic attributes in queries?

I'm not sure if synthetic attributes are capable of being used this way, but I have an account model with two boolean fields, :is_class_accout and :is_expense_account.
I created a an attribute which is not persisted in teh db called :is_synthetic which return true if either :is_class_account is true or :is_expense_account is true.
What I want to do is write a query like:
Account.find_by_project_id(project_id).where(is_synthetic: true)
This returns a PG error because the resulting query is looking for the is_synthetic field in the db, and of course it's not there.
Am I doing something wrong, or is this expected behavior?
The code I am using is:
class Account < ApplicationRecord
attribute :is_synthetic, :boolean
def is_synthetic
self.is_class_account || self.is_expense_account
end
The behaviour is expected. Two things here:
The example query is wrong since find_by_project_id would return the first matching Account record and not a collection to call where on.
In your case all you have to do is retrieve the Account records and then filter them:
class Account < ApplicationRecord
def is_synthetic
self.is_class_account || self.is_expense_account
end
end
# Returns an Array of accounts
Account.where(project_id: <queried_project_id>).select(&:is_synthetic)

Inserting Nested Object Efficiently

If I have the following schema:
class Sinbad < ActiveRecord::Base
has_many :tinbads
accepts_nested_attributes_for: :tinbads
end
class Tinbad < ActiveRecord::Base
belongs_to :sinbad
has_many :pinbads
accepts_nested_attributes_for: :pinbads
end
class Pinbad < ActiveRecord::Base
belongs_to :tinbad
end
and it's not uncommon for a Tinbad to have a few hundred Pinbads, is there a common way to create a nested Sinbad without invoking hundreds of queries?
I've come to the sad understanding that Active Record doesn't support batch inserts but is there a way around this that doesn't involve handwritten SQL? I've looked at https://github.com/zdennis/activerecord-import, but it doesn't support nested objects. Currently, the SinbadController#create action averages >400 insert transactions and it's the most common action used.
Here's an example of what I want not to happen:
https://gist.github.com/adamkuipers/12578343d31a651bee4a
Instead of inserting into the photos table N times, I want to insert only once.
I'm having the exact same problem. I'm parsing big spreadsheets and the schema used to store the data is nested, so I'm inserting only a single "Sinbad" but thousands of "Pinbad" can get inserted at once...
What I came up to speed up the insert is to bulk insert the bottom leaves of the schema (visualize the schema as a tree), as this must be the model with the highest amount of instances to create - in your case, the Pinbad instances. We cannot bulk insert middle leaves as bulk insert doesn't allow to fetch the ids of the multiplie models inserted (see discussion here concerning postgresql for instance). So it's not ideal, but that's the only way I found to make the inserts more efficient (without changing the schema itself).
You'll have to remove accepts_nested_attributes_for as you need to save the objects yourself, and it's convenient to use activerecord-import for the bulk insert:
class Sinbad
#
# Let's imagine you still receive the params as if you were using accepts_nested_attributes,
# Meaning :pinbads_attributes will be nested under :tinbads_attributes
# that will be nested under :sinbad
#
def self.efficient_create params
# I think AR doesn't like when attributes doesn't exist,
# so we should keep the tinbads attributes somewhere else
tinbads_attributes = params[:tinbads_attributes]
params.delete :tinbads_attributes
sinbad = self.create! params
# Array that will contain the attributes of the pinbads to bulk insert
pinbads_to_save = []
# ActiveRecords-Import needs to know which cols of Pinbad you insert
pinbads_cols = [:tinbad_id, :name, :other]
# We need to manually save the tinbads one by one,
# but that's what happen when using accepts_nested_attributes_for
tinbads_attributes.each do |attrs|
pinbads_attribute = attrs[:pinbads_attributes]
attrs.delete :pinbads_attibutes
tinbad = sinbad.tinbads.create! attrs
pinbads_attributes.each do |p_attrs|
# Take care to put the attributes
# in the same order than the pinbad_cols array
pinbads_to_save << [tinbad.id, p_attrs[:name], p_attrs[:other]]
end
end
# Now we can bulk insert the pinbads, using activerecord-import
Pinbad.import_without_validations_or_callbacks pinbad_cols, pinbads_to_save
end
end
That's what I've done in my situation and as the last level in the schema hierarchy has the most instances to create, the overall insert time was greatly reduced. In your case, you would replace the ~400 inserts of Pinbad with 1 bulk insert.
Hope that helps, and I'm open to any suggestion or alternative solution!

Rails Order by frequency of a column in another table

I have a table KmRelationship which associates Keywords and Movies
In keyword index I would like to list all keywords that appear most frequently in the KmRelationships table and only take(20)
.order doesn't seem to work no matter how I use it and where I put it and same for sort_by
It sounds relatively straight forward but i just can't seem to get it to work
Any ideas?
Assuming your KmRelationship table has keyword_id:
top_keywords = KmRelationship.select('keyword_id, count(keyword_id) as frequency').
order('frequency desc').
group('keyword_id').
take(20)
This may not look right in your console output, but that's because rails doesn't build out an object attribute for the calculated frequency column.
You can see the results like this:
top_keywords.each {|k| puts "#{k.keyword_id} : #{k.freqency}" }
To put this to good use, you can then map out your actual Keyword objects:
class Keyword < ActiveRecord::Base
# other stuff
def self.most_popular
KmRelationship.
select('keyword_id, count(keyword_id) as frequency').
order('frequency desc').
group('keyword_id').
take(20).
map(&:keyword)
end
end
And call with:
Keyword.most_popular
#posts = Post.select([:id, :title]).order("created_at desc").limit(6)
I have this listed in my controller index method which allows the the order to show the last post with a limit of 6. It might be something similar to what you are trying to do. This code actually reflects a most recent post on my home page.

More efficient Active Record query for large number of columns

I'm trying to work out a more efficient way to add a note count, with a couple of simple where conditions applied to the query. This can take forever, though, as there are as many as 20K records to iterate over. Would welcome any thinking on this.
def reblog_array(notes)
data = []
notes.select('note_type, count(*) as count').where(:note_type => 'reblog', :created_at => Date.today.years_ago(1)..Date.today).group('DATE(created_at)').each do |n|
data << n.count
end
return data
end
This is what's passed to reblog_array(notes) from my controller.
#tumblr = Tumblr.find(params[:id])
#notes = Note.where("tumblr_id = '#{#tumblr.id}'")
From what I can tell, you are trying to calculate how many reblogs/day this Tumblr account/blog had? If so,
notes.where(:note_type => 'reblog', :created_at => Date.today.years_ago(1)..Date.today).group('DATE(created_at)').count.values
should give you the right result, without having to iterate over the result list again. One thing to note, your call right now won't indicate when there are days with 0 reblogs. If you drop the call to #values, you'll get a hash of date => count.
As an aside and in case you didn't know, I'd also suggest making more use of the ActiveRecord relations:
Class Tumblr
has_many :notes
end
#tumblr = Tumblr.find(params[:id])
#notes = #tumblr.notes
this way you avoid writing code like Note.where("tumblr_id = '#{#tumblr.id}'"). It's best to avoid string-interpolated parameters, in favour of code like Note.where(:tumblr_id => #tumblr.id) or Note.where("tumblr_id = ?", #tumblr.id) to leave less chance that you'll write code vulnerable to SQL injection

Ruby On Rails: How to run safe updates

I'm using RoR and I want to do concurrency safe update queries. For example when I have
var user = User.find(user_id)
user.visits += 1
I get the following SQL code:
SELECT * FROM Users WHERE ID=1 -- find user's visits (1)
UPDATE Users SET Visits=2 WHERE ID=1 -- 1+1=2
But if there are several queries taking place at the same time, there will be locking problems.
According to RoR API I can use :lock => true attribute, but that's not what I want.
I found an awesome function update_counters:
User.update_counters(my_user.id, :visits => 1)
This gives the following SQL code
UPDATE Users SET Visits=Visits+1 WHERE ID=#something
Works great!
Now my question is how can I override the += function to do the update_counters thing instead?
Because
user.visits += 1 # better
User.update_counters(my_user.id, :visits => 1) # than this
UPDATE
I just created the following function
class ActiveRecord::Base
def inc(column, value)
User.update_counters(self.id, column => value)
end
end
Are there any other better ideas?
Don't know about better ideas, but I would define that method to the User class, not the ActiveRecord. And maybe increment_counter (that uses update_counters) would make it a bit more readable?
def inc(column, value)
self.increment_counter(column, value)
end
Haven't tested that and not saying this is definitely better idea, but that's probably how I'd do it.
Update:
And AFAIK you can't override the "+=", because "a += b" just a shortcut for "a = a + b" and you probably don't want to override "=" to use the update_counters :)