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!
Related
I'm trying to write a migration to update a boolean field where the field will mean the exact opposite of what it means currently. Therefore I need to toggle every record to update the records where this field is true to false and vice-versa.
Example:
class ChangePostsVisibleToArchived < ActiveRecord::Migration[6.1]
def change
rename_column :posts, :visible, :archived
# Toggle all tracked topics
# What I DON'T want to do:
Post.all.each { |post| post.toggle! :archived }
end
end
The way I described above will generate one SQL command per Post record.
Is there a way I can toggle all records within a single SQL command using rails ActiveRecord syntax?
Post.update_all "archived = NOT archived"
I am building a Rails app with the following models:
# vote.rb
class Vote < ApplicationRecord
belongs_to :person
belongs_to :show
scope :fulfilled, -> { where(fulfilled: true) }
scope :unfulfilled, -> { where(fulfilled: false) }
end
# person.rb
class Person < ApplicationRecord
has_many :votes, dependent: :destroy
def self.order_by_votes(show = nil)
count = 'nullif(votes.fulfilled, true)'
count = "case when votes.show_id = #{show.id} AND NOT votes.fulfilled then 1 else null end" if show
people = left_joins(:votes).group(:id).uniq!(:group)
people = people.select("people.*, COUNT(#{count}) AS people.vote_count")
people.order('people.vote_count DESC')
end
end
The idea behind order_by_votes is to sort People by the number of unfulfilled votes, either counting all votes, or counting only votes associated with a given Show.
This seem to work fine when I test against SQLite. But when I switch to Postgres I get this error:
Error:
PeopleControllerIndexTest#test_should_get_previously_on_show:
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column people.vote_count does not exist
LINE 1: ...s"."show_id" = $1 GROUP BY "people"."id" ORDER BY people.vot...
^
If I dump the SQL using #people.to_sql, this is what I get:
SELECT people.*, COUNT(nullif(votes.fulfilled, true)) AS people.vote_count FROM "people" LEFT OUTER JOIN "votes" ON "votes"."person_id" = "people"."id" GROUP BY "people"."id" ORDER BY people.vote_count DESC
Why is this failing on Postgres but working on SQLite? And what should I be doing instead to make it work on Postgres?
(PS: I named the field people.vote_count, with a dot, so I can access it in my view without having to do another SQL query to actually view the vote count for each person in the view (not sure if this works) but I get the same error even if I name the field simply vote_count.)
(PS2: I recently added the .uniq!(:group) because of some deprecation warning for Rails 6.2, but I couldn't find any documentation for it so I am not sure I am doing it right, still the error is there without that part.)
Are you sure you're not getting a syntax error from PostgreSQL somewhere? If you do something like this:
select count(*) as t.vote_count from t ... order by t.vote_count
I get a syntax error before PostgreSQL gets to complain about there being no t.vote_count column.
No matter, the solution is to not try to put your vote_count in the people table:
people = people.select("people.*, COUNT(#{count}) AS vote_count")
...
people.order(vote_count: :desc)
You don't need it there, you'll still be able to reference the vote_count just like any "normal" column in people. Anything in the select list will appear as an accessor in the resultant model instances whether they're columns or not, they won't show up in the #inspect output (since that's generated based on the table's columns) but you call the accessor methods nonetheless.
Historically there have been quite a few AR problems (and bugs) in getting the right count by just using count on a scope, and I am not sure they are actually all gone.
That depends on the scope (AR version, relations, group, sort, uniq, etc). A defaut count call that a gem has to generically use on a scope is not a one-fit-all solution. For that known reason Pagy allows you to pass the right count to its pagy method as explained in the Pagy documentation.
Your scope might become complex and the default pagy collection.count(:all) may not get the actual count. In that case you can get the right count with some custom statement, and pass it to pagy.
#pagy, #records = pagy(collection, count: your_count)
Notice: pagy will efficiently skip its internal count query and will just use the passed :count variable.
So... just get your own calculated count and pass it to pagy, and it will not even try to use the default.
EDIT: I forgot to mention: you may want to try the pagy arel extra that:
adds specialized pagination for collections from sql databases with GROUP BY clauses, by computing the total number of results with COUNT(*) OVER ().
Thanks to all the comments and answers I have finally found a solution which I think is the best way to solve this.
First of, the issue occurred when I called pagy which tried to count my scope by appending .count(:all). This is what caused the errors. The solution was to not create a "field" in select() and use it in .order().
So here is the proper code:
def self.order_by_votes(show = nil)
count = if show
"case when votes.show_id = #{show.id} AND NOT votes.fulfilled then 1 else null end"
else
'nullif(votes.fulfilled, true)'
end
left_joins(:votes).group(:id)
.uniq!(:group)
.select("people.*, COUNT(#{count}) as vote_count")
.order(Arel.sql("COUNT(#{count}) DESC"))
end
This sorts the number of people on the number of unfulfilled votes for them, with the ability to count only votes for a given show, and it works with pagy(), and pagy_arel() which in my case is a much better fit, so the results can be properly paginated.
I have linked list kind of structure
defmodule Data.Record do
use Data.Web, :model
alias Data.{Record, Repo}
schema "records" do
field(:date_start, :date)
field(:date_end, :date)
field(:change_reason, :string)
field(:is_active, :boolean, default: true)
field(:notes, :string)
belongs_to(
:changed_from,
Data.Record,
foreign_key: :changed_from_id
)
belongs_to(
:changed_to,
Data.Record,
foreign_key: :changed_to_id
)
timestamps()
end
end
But the problem is we need all the nested records preloaded dynamically. e.g the list can record1 changed_to -> record2 changed_to -> record 3 changed_to. But ecto doesnt/cant preload dynamically e.g record |> preload([{:changed_to, :changed_to}])
What is the best way/workaround to preload all the linked changed_to records?
Well, the most (dirty) workaround would be something like this. It builds the arguments for preload to a certain depth:
def preload_args(relation, max_level \\ 50) do
preload_args(relation, max_level - 1, relation)
end
defp preload_args(_relation, level, acc) when level <= 0, do: acc
defp preload_args(relation, level, acc) do
preload_args(relation, level - 1, [{relation, acc}])
end
To use it:
Repo.preload record, Record.preload_args(:changed_to)
This will preload every :changed_to relation to a certain level or until there are no more. Of course this is not the solution you would really like to use because it performs a query for every preload and you don't know how long the chain will be upfront, might be much longer than 50 steps.
(please don't roast me for this code/suggestion, you specifically asked for workarounds too. ;)
I think that this comment about a 'closure table' by Aetherus, which pointed me to this article will probably lead you to a better solution. It also strengthens my presumption that you don't need to store both parent and child ids in the first place, the parent_id alone should be enough. That would also make it easier to insert a new Record: you don't need to update the parent too.
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)
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.