granite icon indicating copy to clipboard operation
granite copied to clipboard

Unable to easily delete has_many without iterating and deleting each

Open robacarp opened this issue 7 years ago • 8 comments

This pattern isn't ideal:

class User < Granite::ORM::Base
  has_many :posts

  before_destroy :clean_up_posts

  def clean_up_posts
    posts.map &.destroy
  end
end

class Post < Granite::ORM::Base
  belongs_to :user
end

Because it causes N delete queries to be executed. It would be nice to have a way to destroy an entire collection in one go. So I tried some SQL:

  def clean_up_posts
    query = <<-SQL
    DELETE FROM posts WHERE user_id = ?
    SQL

    Post.exec query, [id]
  end

This may be a completely unrelated issue, but the exec and scalar query methods aren't parameterized on the models, so:

wrong number of arguments for 'Post.exec' (given 2, expected 0..1)
Overloads are:
 - Granite::ORM::Querying#exec(clause = "", &block)

As a result, I ended up with this:

  def clean_up_posts
    query = <<-SQL
    DELETE FROM posts WHERE user_id = #{id}
    SQL

    Post.exec query
  end

I don't think this is immediately going to result in a compromisable injection vulnerability...in my case. But it's probably worth adding parameterized exec and scalar methods to the base.

robacarp avatar Jan 27 '18 01:01 robacarp

Wouldnt the better way to handle this be to ensure DB foreign key Constraints are properly set to cascade? Keep in mind cascade also has to deal with update contingencies too. This is something modern RDBMS handle well already. Why duplicate functionality?

shayneoneill avatar Aug 11 '18 22:08 shayneoneill

@shayneoneill leveraging RDBMS constraints could be a great way to handle this

robacarp avatar Aug 13 '18 13:08 robacarp

@shayneoneill @robacarp I think adding db constraints in the migration scripts would be perfect. I always hated rails for not properly supporting RDBMS constraints. We should look into this solution further.

Maybe we can add this as a feature of #182 WDYT?

drujensen avatar Sep 09 '18 21:09 drujensen

I'm pretty sure it should be easy to implement here: https://github.com/amberframework/granite/blob/master/src/granite/association_collection.cr something like:

user.posts.delete_all

Adam-Stomski avatar Sep 09 '18 21:09 Adam-Stomski

@Adam-Stomski yeah, we can add a delete_all here fairly easily. We can also support cascade deleting children rows on a has_many relationship.

I guess the question is if we should take advantage of the RDBMS to handle the relationship integrity. For example, you can specify a cascading delete in the relationship in the database:

CREATE TABLE rooms (
    room_no INT PRIMARY KEY AUTO_INCREMENT,
    room_name VARCHAR(255) NOT NULL,
    building_no INT NOT NULL,
    FOREIGN KEY (building_no)
        REFERENCES buildings (building_no)
        ON DELETE CASCADE
);

The nice thing about this is that your data will never get out of sync (unlike Rails) where someone might access the database directly and delete a row without deleting the children creating orphans.

But, How can we do this without controlling the migration scripts?

drujensen avatar Sep 10 '18 04:09 drujensen

Rails did eventually add support for most database level constraints, but it was vacant for far too long.

But, How can we do this without controlling the migration scripts?

The answer is, I don't think we do. If someone wants to add that to the database, they'll have to add that manually. Adding #delete_all to the association_collection is a great next step though. It'll be needed even if granite someday gets control over db constraints.

robacarp avatar Sep 10 '18 21:09 robacarp

I guess the question then is, should granite follow ActiveRecord and add two methods:

  1. destroy_all - just runs destroy on each record in loop, runs callbacks
  2. delete_all - deletes all records in a single query, no callbacks

wdyt? I will have some time this week, can add it

Adam-Stomski avatar Sep 10 '18 21:09 Adam-Stomski

@robacarp if memory serves me right, David Hansson use to harbor some really bad ideas about database consistency, feeling that constraints belonged in the code not the database. Something I personally consider reckless. Constraints should be opt-out, not opt-in. It can't be assumed the coder understands the implications of orphaning records well enough to make the right decisions around deploying transactions and sanity checking to avoid corruption. Django does (and always has done) the right thing here, creating constraints and appropriate rules at the SQL level to make sure programmers won't shoot themselves in the foot. Unless, of course, they chose not and disable it. And occasionally there are use cases for that too (Ie existing database, or strange schemas)

shayneoneill avatar Feb 06 '19 07:02 shayneoneill