Ian Vaughan
Posted on August 14, 2020
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:
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
fromCompany
toUser
and -
record_id
from the Company id28
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!)
Posted on August 14, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.