DEV Community

Cover image for Moving has_many_attached to a different model
Ian Vaughan
Ian Vaughan

Posted on

Moving has_many_attached to a different model

has_many_attached is an amazingly easy to way to save attachments to your models.

Specifies the relation between multiple attachments and the model.

-- https://edgeapi.rubyonrails.org/classes/ActiveStorage/Attached/Model.html#method-i-has_many_attached

But with all that ease, comes a load of magic under the hood, none of which you really need to care about for day to day usage.
Until you need to do something more advance, like move the association from one model to another...

Under the covers, this relationship is implemented as a has_many association to a ActiveStorage::Attachment record and a has_many-through association to a ActiveStorage::Blob record. These associations are available as photos_attachments and photos_blobs. But you shouldn't need to work with these associations directly in most circumstances.

Setup

Say we have a Company model with some filed_accounts attached:

class Company < ApplicationRecord
  has_many_attached :filed_accounts
end

To get setup from scratch or more info see here: https://edgeguides.rubyonrails.org/active_storage_overview.html#setup

Database

Currently the schema looks like:

Alt Text

And data

Company

id name ...
28 Pipe Piper ...

ActiveStorage::Attachment

id name record_type record_id blob_id
20 'filed_accounts' 'Company' 28 30
23 'filed_accounts' 'Company' 28 33

ActiveStorage::Blob

id ...
30 ...
33 ...

See the ActiveStorage::Attachment#record_id points to our model and #blob_id to the Blob.

 Usage

Attachments are easy to use, we can see the Attached :

> Company.find(28).filed_accounts
=> #<ActiveStorage::Attached::Many:
 @name="filed_accounts",
 @record=
  #<Company:
   id: 28,
   ...>>

More interesting are the actual attachments :

> Company.find(28).filed_accounts.attachments
=> [<ActiveStorage::Attachment: id: 20, name: "filed_accounts", record_type: "Company", record_id: 28, blob_id: 30>,
    <ActiveStorage::Attachment: id: 23, name: "filed_accounts", record_type: "Company", record_id: 28, blob_id: 33>]

Which we can see under the hood goes via the record_id and record_type :

Company.find(28).filed_accounts.attachments.to_sql
=> "SELECT \"active_storage_attachments\".* 
    FROM \"active_storage_attachments\" 
    WHERE \"active_storage_attachments\".\"record_id\" = 28 
      AND \"active_storage_attachments\".\"record_type\" = 'Company' 
      AND \"active_storage_attachments\".\"name\" = 'filed_accounts'"

We can also see the blobs which is the actual data

> Company.find(28).filed_accounts.blobs
=> [#<ActiveStorage::Blob:
  id: 30,
  key: "udi431282hya1l68dt16fi4ee4zd",
  filename: "my_doc.pdf",
  content_type: "application/pdf",
  metadata: {"identified"=>true, "analyzed"=>true},
  byte_size: 167766,
  checksum: "jy+j/AFI9nc8+5afLEpqSw==">,
 #<ActiveStorage::Blob:
  id: 33,
  key: "mn25fl9u08lwsq8lbvm5wo93whpn",
  filename: "my_doc2.pdf",
  content_type: "application/pdf",
  metadata: {"identified"=>true, "analyzed"=>true},
  byte_size: 77490,
  checksum: "0n98/tzywedKJzOT/X0vSw==">]

And under the hood the blobs goes via the attachments :

> Company.find(28).filed_accounts.blobs.to_sql
=> "SELECT \"active_storage_blobs\".* 
    FROM \"active_storage_blobs\" 
    INNER JOIN \"active_storage_attachments\" 
    ON \"active_storage_blobs\".\"id\" = \"active_storage_attachments\".\"blob_id\" 
    WHERE \"active_storage_attachments\".\"record_id\" = 28 
      AND \"active_storage_attachments\".\"record_type\" = 'Company' 
      AND \"active_storage_attachments\".\"name\" = 'filed_accounts'"

Move it to a different model

And we want to move the attachments to the User model, first, add the has_many_attached :

 class User < ApplicationRecord
+  has_many_attached :filed_accounts
 end

We now need to update the ActiveStorage::Attachment rows:

  • record_type from Company to User and
  • record_id from the Company id 28 to the User id linked to the company.

So in a migration file:

def up
  Company.all.each do |company|
    company.filed_accounts.attachments.update_all(
      record_type: 'User', 
      record_id: company.user.id
    )
  end
end

(You can make the migration more efficient if you wish!)

Top comments (0)