Using after_destroy to update the tree of a deleted node

I have a Post model, that each post belongs to a tree powered by ancestry. Each post object is either a root, parent, child or any descendant or ancestor thereof. I have an attribute called max_depth on each post object. I also have a method that needs to calculate that max_depth for the entire tree whenever a node is added/updated/deleted.

I created a callback for the added & updated cases and that works well. The issue I am having is whenever a node is deleted. I keep getting an error.

This is my after_destroy callback:

after_destroy :update_max_depth_on_entire_tree 

The definition is:

  def update_max_depth_on_entire_tree
    root = self.root
    root.check_or_update_max_tree_depth
    root.descendants.each { |c| 
      c.check_or_update_max_tree_depth
    }
  end

  def check_or_update_max_tree_depth
    update_columns(max_tree_depth: last_depth)      
  end

  def last_depth
    if child_ids.empty?
      return depth
    else
      return children.map{|c| c.last_depth}.max
    end
  end

Whenever I try to delete an object, I get this error:

ActiveRecord::ActiveRecordError at /posts/detecting-ramsay-is-offered-ceo-job
cannot update on a new record object

This is what the Server log looks like:

Started DELETE "/posts/detecting-ramsay-is-offered-ceo-job" for 127.0.0.1 at 2015-01-05 09:56:24 -0500
Processing by PostsController#destroy as HTML
  Parameters: {"authenticity_token"=>"2xRY+A=", "id"=>"detecting-ramsay-is-offered-ceo-job"}
  User Load (0.5ms)  SELECT  "users".* FROM "users"  WHERE "users"."id" = 3  ORDER BY "users"."id" ASC LIMIT 1
   (2.9ms)  SELECT COUNT(*) FROM "roles" INNER JOIN "users_roles" ON "roles"."id" = "users_roles"."role_id" WHERE "users_roles"."user_id" = $1 AND (((roles.name = 'admin') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL)))  [["user_id", 3]]
   (2.4ms)  SELECT COUNT(*) FROM "roles" INNER JOIN "users_roles" ON "roles"."id" = "users_roles"."role_id" WHERE "users_roles"."user_id" = $1 AND (((roles.name = 'editor') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL)))  [["user_id", 3]]
  Post Load (0.9ms)  SELECT  "posts".* FROM "posts"  WHERE "posts"."slug" = 'detecting-ramsay-is-offered-ceo-job'  ORDER BY "posts"."id" ASC LIMIT 1
   (0.2ms)  BEGIN
  Post Load (5.2ms)  SELECT "posts".* FROM "posts"  WHERE (("posts"."ancestry" ILIKE '69/%' OR "posts"."ancestry" = '69'))
  Role Load (0.6ms)  SELECT "roles".* FROM "roles"  WHERE "roles"."resource_id" = $1 AND "roles"."resource_type" = $2  [["resource_id", 69], ["resource_type", "Post"]]
  FriendlyId::Slug Load (2.9ms)  SELECT "friendly_id_slugs".* FROM "friendly_id_slugs"  WHERE "friendly_id_slugs"."sluggable_id" = $1 AND "friendly_id_slugs"."sluggable_type" = $2  ORDER BY "friendly_id_slugs".id DESC  [["sluggable_id", 69], ["sluggable_type", "Post"]]
  SQL (1.5ms)  DELETE FROM "friendly_id_slugs" WHERE "friendly_id_slugs"."id" = $1  [["id", 47]]
  SQL (1.2ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 69]]
  Post Load (0.4ms)  SELECT "posts"."id" FROM "posts"  WHERE "posts"."ancestry" = '69'
   (0.2ms)  ROLLBACK
Completed 500 Internal Server Error in 281ms

ActiveRecord::ActiveRecordError - cannot update on a new record object:
  activerecord (4.1.6) lib/active_record/persistence.rb:272:in `update_columns'
   () /app/models/post.rb:122:in `check_or_update_max_tree_depth'
   () /app/models/post.rb:130:in `update_max_depth_on_entire_tree'

This is what my routes look like:

posts_path	GET	/posts(.:format)	posts#index
POST	/posts(.:format)	posts#create
new_post_path	GET	/posts/new(.:format)	posts#new
edit_post_path	GET	/posts/:id/edit(.:format)	posts#edit
post_path	GET	/posts/:id(.:format)	posts#show
PATCH	/posts/:id(.:format)	posts#update
PUT	/posts/:id(.:format)	posts#update
DELETE	/posts/:id(.:format)	posts#destroy
GET	/posts/:id(.:format)	redirect(301, /%{id})
GET	/:friendly_id(.:format)	posts#show
GET	/posts/:friendly_id(.:format)	posts#show
GET	/:name(.:format)	posts#show
root_path	GET	/	posts#index

And this is what my PostController#Destroy looks like:

  def destroy
    @post = Post.find(params[:id])
    @post.destroy
    respond_to do |format|
      format.html { redirect_to posts_url, notice: 'Report was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

I believe the issue is with this portion of the code:

  def check_or_update_max_tree_depth
    update_columns(max_tree_depth: last_depth)      
  end

What the error seems to be saying is that somewhere along the chain it encounters a new record, and it can’t execute update_columns(max_tree_depth: last_depth) on an object that is a new record.

How do I update the rest of the tree that the recently deleted node belonged to, without getting this error?

The most straightforward solution would be to wrap the update_columns statement in a check for new_record?, but I wouldn’t do that, because your code here is already a lot of stuff to put into a callback.

I think would create a new PostTreeUpdate class and have the callback create a PostTreeUpdate object with the current Post, and then let that object handle the work of updating max tree depth, etc. This way your callback logic doesn’t have to know about children of the object, etc. It can just delegate that work away to another class purpose-built to handle the task of updating.

Well the first thing I want to do is to get it working.

Then I can always refactor.