Reassociate all related models in rails - ruby-on-rails

Ok, We f&^%$**&ed up.
We lost a bunch of user records. At some point, an integration file ran which re-inserted some of the lost records.
The problem is that the new users have a different ID than the original user, so all the existing related content for the old User id has been orphaned. I now need to go back in and reassociate all the orphaned stuff to the new User id. It won't be enough to simply give the new user the old Id from backup, because there will be new content associated to the new User Id.
We know the reflect_on_all_associations method, but that is hard to use for finding stuff. However, this could be a starting point for a script of some kind.
Any clues on how to have a method return all models related to a particular model based on associations, without having to specify or know those associations?

Here's a way to use reflect_all_associations: You can iterate through the associations, select only the has_many and has_one, and then update those records. Here's a helper class to do the job which you can execute by calling AssociationFixer.new(user_to_destroy, original_user_id).fix_associations:
class AssociationFixer
USER_ASSOCIATIONS = User.reflect_on_all_associations
def initialize(user_to_destroy, original_user_id)
#user_to_destroy = user_to_destroy
#original_user_id = original_user_id
end
def fix_associations
USER_ASSOCIATIONS.each do |association|
next if association.options.has_key? :through
if association.macro == :has_many
fix_has_many(association)
elsif association.macro == :has_one
fix_has_one(association)
end
end
end
def fix_has_many(association)
#user_to_destroy.send(association.name).each do |record|
if association.options.has_key? :foreign_key
record.send(assignment_method(association.foreign_key), #original_user_id)
else
record.user_id = #original_user_id
end
record.save
end
end
def fix_has_one(association)
if association.options.has_key? :foreign_key
#user_to_destroy.send(association.name).send(assignment_method(association.foreign_key), #original_user_id)
else
#user_to_destroy.send(assignment_method(association.name.user_id), #original_user_id)
end
record.save
end
def assigment_method(method_name)
method_name.to_s + '='
end
end

This sounds like a problem for SQL. I would look at importing your backup tables into the database in a separate table, if it's possible to join them on a column, maybe email or user_name or something. Then you can run a select based on the old id and update to the new id. Best of luck!

Related

How do I replace an ActiveRecord association member in-memory and then save the association

I have a has_may through association and I'm trying to change records in the association in memory and then have all the associations updated in a single transaction on #save. I can't figure out how to make this work.
Here's a simplifiction of what I'm doing (using the popular Blog example):
# The model
class Users < ActiveRecord::Base
has_many :posts, through: user_posts
accepts_nested_attributes_for :posts
end
# The controller
class UsersController < ApplicationController
def update
user.assign_attributes(user_params)
replace_existing_posts(user)
user.save
end
private
def replace_existing_posts(user)
user.posts.each do |post|
existing = Post.find_by(title: post.title)
next unless existing
post.id = existing
post.reload
end
end
end
This is a bit contrived. The point is that if a post that the user added already exists in the system, we just assign the existing post to them. If the post does not already exist we create a new one.
The problem is, that when I call user.save it saves any new posts (and the user_post association) but doesn't create the user_post association for the existing record.
I've tried to resolve this by adding has_many :user_posts, autosave: true to the User model, but despite the documented statement "When :autosave is true all children are saved", that doesn't reflect the behavior I see.
I can make this work, with something hacky like this, but I don't want to save the association records separately (and removing and replacing all associations would lead to lots of callback I don't want to fire).
posts = user.posts.to_a
user.posts.reset
user.posts.replace(posts)
I've trolled through the ActiveRecord docs and the source code and haven't found a way to add records to a has_many through association that create the mapping record in memory.
I finally got this to work, just by adding the association records manually.
So now my controller also does this in the update:
user.posts.each do |post|
next unless post.persisted?
user.user_posts.build(post: post)
end
Posting this as an answer unless someone has a better solution.

How can I idiomatically replace a record/row with another record/row while maintaining the original ids and references?

I have an ActiveRecord object with 3 "has_many" relations/sub ActiveRecords.
My goal is to have "clones" that are able to modify their own version of the og_actions/og_objects/ob_stories for testing purposes, but also have the ability to push the data from the parent to all clones to overwrite any changes that were made.
My assumed method of doing this is to update those relationships with the data from another ActiveRecord, however, I don't want to change the IDs or the Foreign_key references when I copy over the data.
How do I do that in an idiomatic way?
Or perhaps should I just delete all records and create new ones with the old ids? If so whats the best way to do that?
Here is the code I'm currently using, and its not working:
class App < ActiveRecord::Base
...
belongs_to :user
has_many :og_actions
has_many :og_objects
has_many :og_stories
has_many :log_reports
has_many :clones, class_name: "App", foreign_key: "parent_id"
...
def populate_clones
self.clones.each do |c|
p "updating ->"
self.og_actions.each_with_index do | oa, ai |
new_og_action = OgAction.create(oa.attributes.merge({app_id:c.id, id: c.og_actions[ai].id }))
c.og_actions[ai] = new_og_action
end
self.og_objects.each_with_index do |oo, oi|
new_og_object = OgObject.create(oo.attributes.merge({app_id:c.id, id: c.og_objects[oi].id }))
c.og_objects[oi] = new_og_object
end
self.og_stories.each_with_index do | s, si|
new_og_story = OgStory.create(s.attributes.merge({app_id:c.id, id: c.og_stories[si].id }))
s.story_variants.each do_with_index do |v, vi|
new_variant = StoryVariant.create(v.attributes.merge({og_story_id:new_og_story.id, id:c.og_stories[si].story_variants[vi].id}))
new_og_story.story_variants[vi] = new_variant
end
c.og_stories[si] = new_og_story
end
c.save
end
p "end Update"
end
I've also tried using the replace function, as well as a simple assignment of c.og_objects = self.og_objects nothing seems to be working properly. It either creates a new record creating duplication, replaces all the references, so the parent ActiveRecord loses its reference, or gets a "duplicate id" error.
This is tricky. I keep thinking about more and more cases in which there could be issues. Anyway here is a start:
def sync_clones
clones.each do |clone|
# Destroy any og_actions for clone that are no longer in the parent app
clone.og_actions.where.not(parent_id: og_actions.ids).destroy_all
# Create or update a og_action clone for app clone
og_actions.each do |og_action|
clone_attributes = og_action.dup.attributes.except("id").merge(parent_id: og_action.id)
existing = clone.og_actions.find_by(parent_id: og_action.id)
if existing
existing.update(clone_attributes)
else
clone.og_actions.build(clone_attributes)
end
end
# ...
end
end
This will update the clone faithfully and not create unnecessary records. It does require you to keep track of the parent og_action record. This is because you can't rely on og_actions index to identify the matching clone record (what would happen if you destroy one og_action or add one, or if the order is changed another way).

broken create, save, update, and destroy on Join record when using Postgres UUIDs in Rails

I'm creating uids using
create_table :users, { id: false } do |t|
t.uuid :uid, default: 'uuid_generate_v4()'
... other columns
and setting self.primary_key = :uid in the models.
In general this works fine with ActiveRecord and I write has_many and belongs_to associations fine. However, when crossing a join table (i.e. has_many ... through:, I need to write custom SQL to get records.
I've figured out that I can in general do this by writing custom SQL, i.e. SELECT * FROM main_table JOIN join_table ON main_table.uid = cast(join_table.uid AS uuid) WHERE condition=true)
I've just recently realized that ActiveRecord's create, destroy, save and update dont work on the join model.
I have patched the four methods so they work, but it's too complex a sequence for my taste and probably unoptimal. Here are my patches:
def save(*args)
# save sometimes works if it is called twice,
# and sometimes works the first time but says theres an error
super(*args) unless (super(*args) rescue true)
end
Sometimes, save issues a ROLLBACK the first time with no explanation. Then it works the second time. In other situations (I'm not sure why, possibly when updating), the first time it goes through successfully but if called a second time raises a TypeError. See here for another question about this error which doesn't have any answers for how to save a join when using uid instead of id. Here are my other (working) patches.
def create(*args)
attrs = args[0]
raise( ArgumentError, "invalid args to bucket list create" ) unless attrs.is_a?(Hash)
bucket_list_photo = self.class.new(
attrs
)
bucket_list_photo.save
bucket_list_photo = BucketListPhoto.find_by(
bucket_list_uid: bucket_list_photo.bucket_list_uid,
photo_uid: bucket_list_photo.photo_uid
)
return bucket_list_photo
end
def update(*args)
# similar bug to save
attrs = args[0]
raise( ArgumentError, "invalid args to bucket list update" ) unless attrs.is_a?(Hash)
bucket_list_uid = self.bucket_list_uid
photo_uid = self.photo_uid
due_at = self.due_at
self.destroy
bucket_list_photo = self.class.new(
{
bucket_list_uid: bucket_list_uid,
photo_uid: photo_uid,
due_at: due_at
}.merge(attrs)
)
bucket_list_photo.save
bucket_list_photo = self.class.find_by(
photo_uid: photo_uid,
bucket_list_uid: bucket_list_uid
)
return bucket_list_photo # phew
end
def destroy
# patching to fix an error on #destroy, #destroy_all etc.
# the problem was apparently caused by custom primary keys (uids)
# see https://stackoverflow.com/a/26029997/2981429
# however a custom fix is implemented here
deleted_uids = ActiveRecord::Base.connection.execute(
"DELETE FROM bucket_list_photos WHERE uid='#{uid}' RETURNING uid"
).to_a.map { |record| record['uid'] }
raise "BucketListPhoto not deleted" unless (
(deleted_uids.length == 1) && (deleted_uids.first == uid)
)
ActiveRecord::Base.connection.query_cache.clear
# since, the cache isnt updated when using ActiveRecord::Base.connection.execute,
# reset the cache to ensure accurate values, i.e. counts and associations.
end
I even ensured that self.primary_key = :uid in all my models.
I also tried replacing uid with id everywhere and verified that all the specs were passing (though I left in the patch). However, it still failed when I removed the patch (i.e. renaming the uid columns to id did not fix it).
EDIT
In response to some comments I've tried the activeuuid gem (where i got stuck on an error) and decided to totally switch over to ids. This is basically for simplicity's sake since I have pressure to launch this app ASAP.
Still, even with this fix I am required to patch save, create, and update. Actually the delete patch no longer works and I had to remove it (relying on the original). I would definitely like to avoid having to make these patches and I am keeping the bounty open for this reason.
There are pros and cons to retaining both id and uuid. For JSON APIs that expose uuid, using Concerns would be a Rails-ish implementation.
app/models/user.rb
class User < ActiveRecord::Base
include UserConcerns::Uuidable
end
app/models/concerns/user_concerns/uuidable.rb
module UserConcerns::Uuidable
extend ActiveSupport::Concern
included do
before_save :ensure_uuid!
end
def ensure_uuid!
self.uuid = generate_uuid if uuid.blank?
end
def generate_uuid
# your uuid using uuid_generate_v4() here
end
def to_param
uuid
end
end
Above implementation leaves out the uuid generation but I think above answer has a link to that.
I have provided 1 solution to generate UUID, I knew you switched back to id now.
find link for UUID
Yes I agree we can not perform CRUD on joins table why don't you use active records relations to perform CRUD operations.
The important thing to understand is the convention by which Rails implements relationships using ActiveRecord. A book has many characters, and each character belongs to a book, so:
class Book < ActiveRecordBase
has_many :characters
end
class Character < ActiveRecordBase
belongs_to :book
end
Rails now assumes that the characters table will have a foreign key called book_id, which relates to the books table. To create a character belonging to a book:
#book = Book.new(:name=>"Book name")
#character = #book.characters.build(:name=>"Character name")
Now when #book is saved (assuming both #book and #character are valid), a row will be created in both the books and the characters tables, with the character row linked through book_id.
To show that a character also belongs to a user, you could add that relationship to the Character model:
class Character < ActiveRecordBase
belongs_to :book
belongs_to :user
end
Thus Rails now expects characters to also have foreign key called user_id, which points to a users table (which also needs a User model). To specify the user when creating the character:
#book = Book.new(:name=>"Book name")
#character = #book.characters.build(:name=>"Character name",:user=>current_user)
You can also assign the foreign key by calling the corresponding method on the object:
#character.user = current_user
This all works because it follows the Rails conventions for naming models and tables.

Rails preview update associations without saving to database

I want to preview what the model will look like when saved without currently saving to the database.
I am using #event.attributes = because that assigns but does not save attributes for #event to the database.
However, when I also try to assign the audiences association, Rails inserts new records into the audiences_events join table. Not cool. Is there a way to preview what these new associations will look like without inserting into the join table?
Model
class Event < ActiveRecord::Base
has_and_belongs_to_many :audiences # And vice versa for the Audience model.
end
Controller
class EventsController < ApplicationController
def preview
#event = Event.find(params[:id])
#event.attributes = event_params
end
private
def event_params
params[:event].permit(:name, :start_time, :audiences => [:id, :name]
end
end
Possible Solutions?
Possible solutions that I thought of, but don't know how to do:
Using some sort of method that assigns associations, but does not persist them.
disabling all database writes for this one action (I dont know how to do that).
Rolling back all database changes at the end of this action
Any help with these would be great!
UPDATE:
After the reading the great answers below, I ended up writing this service class that assigns the non-nested attributes to the Event model, then calls collection.build on each of the nested params. I made a little gist. Happy to receive comments/suggestions.
https://gist.github.com/jameskerr/69cedb2f30c95342f64a
In these docs you have:
When are Objects Saved?
When you assign an object to a has_and_belongs_to_many association, that object is automatically saved (in order to update the join table). If you assign multiple objects in one statement, then they are all saved.
If you want to assign an object to a has_and_belongs_to_many association without saving the object, use the collection.build method.
Here is a good answer for Rails 3 that goes over some of the same issues
Rails 3 has_and_belongs_to_many association: how to assign related objects without saving them to the database
Transactions
Creating transactions is pretty straight forward:
Event.transaction do
#event.audiences.create!
#event.audiences.first.destroy!
end
Or
#event.transaction do
#event.audiences.create!
#event.audiences.first.destroy!
end
Notice the use of the "bang" methods create! and destroy!, unlike create which returns false create! will raise an exception if it fails and cause the transaction to rollback.
You can also manually trigger a rollback anywhere in the a transaction by raising ActiveRecord::Rollback.
Build
build instantiates a new related object without saving.
event = Event.new(name: 'Party').audiences.build(name: 'Party People')
event.save # saves both event and audiences
I know that this is a pretty old question, but I found a solution that works perfectly for me and hope it could save time to someone else:
class A
has_many :bs, class_name 'B'
end
class B
belongs_to :a, class_name: 'A'
end
a.bs.target.clear
new_bs.each {|new_b| a.bs.build new_b.attributes.except('created_at', 'updated_at', 'id') }
you will avoid autosave that Rails does when you do a.bs = new_bs

How to access a model from another model?

I have two models in ROR, one which is Note and another one which is Access. Each Access has a Note field and a user field. In my index action of the notes controller I want to filter notes owned by the user (done) as well as notes accessible to the user, which I named #accessible_notes.
The following code gives me the correct notes owned by the user, however I cannot get the notes accessible to the user.
Basically, I need to find all the Accesses in which the user is involved and then fetch the corresponding notes. How can I do that?
def index
#notes = Note.where(user: current_user)
#personal_access = Access.where("user_id = ?",current_user.id)
#accessible_notes = []
#personal_access.each do |accessible|
tnote = Note.find(accessible.note_id)
#accessible_notes += tnote if tnote
end
end
class User < ActiveRecord::Base
has_many :accessible_notes, :through => :accesses, :source => :notes
end
#accessible_notes = current_user.accessible_notes
How about
#personal_access.each do |accessible|
#accessible_notes << accessible.note
end
#accessible_notes.flatten!
There might be a faster way using Active Record queries.
And that faster way is in depa's answer.

Resources