Moving has_many_attached to a different model
Tags: rails, ruby, database
Published: 2020-08-14 12:59:22

`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:

```ruby
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](https://dev-to-uploads.s3.amazonaws.com/i/lzqehly9lvq74u08i4qr.png)

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 :

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

More interesting are the actual attachments :

```ruby
> 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` :

```ruby
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 

```ruby
> 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 :

```ruby
> 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` :

```diff
 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:

```ruby
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!)