Grey
Grey

Reputation: 114

Delete a relationship in a Rails model

I'm using rails 5 and I have an ActiveRecord 'Report' with a one-to-many relationship called dosages.

If on a new report:

report.dosages.size   # Returns 0
report.dosages.build
report.dosages.size   # 1, correct
report.dosages.first.destroy
report.dosages.size  # Still 1 !

I understand that it set dosage as 'destroyed' but it's possible to actually remove it from the list of relationship? (I can't save the report on db until later)

Upvotes: 2

Views: 6294

Answers (2)

Alex.U
Alex.U

Reputation: 1701

TL;DR: this will do what you need:

report = Report.first
dosage = report.dosages.build
report.dosages.size // 1
to_delete_dosage = report.dosages.first
report.dosages.delete(to_delete_dosage)
report.dosages.size // 0

For more information, check the docs for ActiveRecord_Associations_CollectionProxy::delete


First, ActiveRecord::Relation#build is an alias for ActiveRecord::Relation#new:

New objects can be instantiated as either empty (pass no construction parameter) or pre-set with attributes but not yet saved (pass a hash with key names matching the associated table column names). In both instances, valid attribute keys are determined by the column names of the associated table – hence you can’t have attributes that aren’t part of the table columns.

if you want the first element deleted you would have to persist it to the database, otherwise rails does exactly what you experienced, set the destroyed flag.

Try the following sequence:

report = Report.first
dosage = report.dosages.build
dosage.save
report.dosages.size   # 1
report.dosages.first.destroy
report.dosages.size  # 0

I set up the equivalent to your question and this is the output:

Your scenario:

2.4.0 :007 > r.dosages.build
 => #<Dosage id: nil, title: nil, created_at: nil, updated_at: nil, report_id: 1>
2.4.0 :008 > r.dosages.size
 => 1
2.4.0 :009 > r.dosages.first.destroy
   (0.1ms)  begin transaction
   (0.0ms)  commit transaction
 => #<Dosage id: nil, title: nil, created_at: nil, updated_at: nil, report_id: 1>
2.4.0 :010 > r.dosages.size
 => 1

My proposal:

2.4.0 :005 > report = Report.first
  Report Load (0.1ms)  SELECT  "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT ?  [["LIMIT", 1]]
 => #<Report id: 1, title: nil, date: nil, created_at: "2018-07-03 14:00:08", updated_at: "2018-07-03 14:00:08">
2.4.0 :006 > report.dosages.count
   (0.1ms)  SELECT COUNT(*) FROM "dosages" WHERE "dosages"."report_id" = ?  [["report_id", 1]]
 => 0
2.4.0 :007 > dosage = report.dosages.build
 => #<Dosage id: nil, title: nil, created_at: nil, updated_at: nil, report_id: 1>
2.4.0 :008 > dosage.save
   (0.1ms)  begin transaction
  SQL (0.7ms)  INSERT INTO "dosages" ("created_at", "updated_at", "report_id") VALUES (?, ?, ?)  [["created_at", "2018-07-03 14:06:08.709323"], ["updated_at", "2018-07-03 14:06:08.709323"], ["report_id", 1]]
   (0.9ms)  commit transaction
 => true
2.4.0 :009 > report.dosages.size
   (0.2ms)  SELECT COUNT(*) FROM "dosages" WHERE "dosages"."report_id" = ?  [["report_id", 1]]
 => 1
2.4.0 :010 > report.dosages.first.destroy
  Dosage Load (0.2ms)  SELECT  "dosages".* FROM "dosages" WHERE "dosages"."report_id" = ? ORDER BY "dosages"."id" ASC LIMIT ?  [["report_id", 1], ["LIMIT", 1]]
   (0.0ms)  begin transaction
  SQL (0.3ms)  DELETE FROM "dosages" WHERE "dosages"."id" = ?  [["id", 1]]
   (0.8ms)  commit transaction
 => #<Dosage id: 1, title: nil, created_at: "2018-07-03 14:06:08", updated_at: "2018-07-03 14:06:08", report_id: 1>
2.4.0 :011 > report.dosages.size
   (0.2ms)  SELECT COUNT(*) FROM "dosages" WHERE "dosages"."report_id" = ?  [["report_id", 1]]
 => 0

I believe this is what you need. The relationships are set like:

dosage.rb
class Dosage < ApplicationRecord
  belongs_to :report
end

report.rb
class Report < ApplicationRecord
  has_many :dosages
end

And the following migrations:

class ManyDosagesToReport < ActiveRecord::Migration[5.0]
  def change
    add_column :dosages, :report_id, :integer
  end
end

In fact, if you try to persist the dosage object that you built but didn't persist, Rails will complain:

RuntimeError: can't modify frozen Hash

due to the destroyed attribute.

Upvotes: 4

Kenigbolo
Kenigbolo

Reputation: 213

If I understand you correctly, you would like to remove the dosage from the db hence rendering the dosage as deleted but would still like to keep the report object? If this is your use case then first you should know that report will stay regardless of the dosage being deleted since its a 1-* (one to many relationship) however you will need to take a look at the difference between delete and destroy which the stackoverflow community has helped in answering here Difference between Destroy and Delete

As a bonus, do take a look at the shebang(!) method in ruby and when to use it e.g. delete! destroy!

Upvotes: 0

Related Questions