Delete_all on a has_many through: relation?

I’m curious if there’s a cleaner way to do this:

class Game
  has_many :players
  has_many :moves, through: players

  def reset_game!
    moves.delete_all
  end
end

class Player
  belongs_to :game
  has_many :moves
end

class Move
  belongs_to :player
end

When I try reset_game! I get an error:

ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection:
  Cannot modify association 'Game#moves' because the source reflection class 'Move' is associated to 'Player' via :has_many.

I’m not entirely sure why it’s giving me an error in the first place. I would have expected SQL kind of like…

DELETE m 
  FROM moves m
  JOIN players p ON p.id = m.players_id 
  WHERE p.game_id = $1

…but maybe there’s an issue with this I’m not familiar with?

I tried…

def reset_game!
  Moves.join(:players).where('players.game_id' => self.id).delete_all
end

And it did what I was after, although it did it via a subquery rather than directly via the joins I was expecting.

  Move Delete All (0.9ms)  DELETE FROM "moves" WHERE "moves"."id" IN (SELECT "moves"."id" FROM "moves" INNER JOIN "players" ON "players"."id" = "moves"."player_id" WHERE "players"."game_id" = $1)  [["game_id", 922]]

In an example of Rubber Ducking, now that I’m saying that out loud, maybe not all the supported SQL servers have the ability to delete with JOINs like what I wrote above? I am using PostgeSQL, if that matters.

If you update just the has_many in your Player class like this:

class Player
  belongs_to :game
  has_many :moves, dependent: :destroy
end

then reset_game! can become:

  def reset_game!
    players.delete_all
  end

Although you might not want to eradicate all the players – perhaps just all the moves! So your hand-tuned DELETE with JOIN would make more sense in that case.

That is the intent, yes! Just delete the moves. Although, to be transparent, I’ve changed the models from my original code into something that’s a little easier to conceptualize.

I’m just surprised that ActiveRecord doesn’t do this natively. I can use the has_many through: to query those objects, so why not be able to delete them? I’m curious as to why the ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection error is a thing in the first place.

Have tried with the same kind of setup from the Northwind database, and sure enough like you mention above it does do the DELETE by using a subquery with a couple JOINs. The setup is Category → Product → OrderDetail, and deleting all OrderDetail where the category is Beverages. Ends up being just over 400 of them:

2.7.5 :007 > OrderDetail.joins(product: :category).where('category.category_name': 'Beverages').delete_all
  OrderDetail Destroy (26.3ms)  DELETE FROM "order_details" WHERE "order_details"."id" IN (SELECT "order_details"."id" FROM "order_details" INNER JOIN "products" ON "products"."id" = "order_details"."product_id" INNER JOIN "categories" "category" ON "category"."id" = "products"."category_id" WHERE "category"."category_name" = $1)  [["category_name", "Beverages"]]
 => 404

Comment from source for ActiveRecord::Associations::ThroughAssociation "

    # We only support indirectly modifying through associations which have a belongs_to source.
    # This is the "has_many :tags, through: :taggings" situation, where the join model
    # typically has a belongs_to on both side. In other words, associations which could also
    # be represented as has_and_belongs_to_many associations.
    #
    # We do not support creating/deleting records on the association where the source has
    # some other type, because this opens up a whole can of worms, and in basically any
    # situation it is more natural for the user to just create or modify their join records
    # directly as required.

That code was added in this commit where you can see additional discussion.

2 Likes