January 31, 2018·5 min read·engineeringrubyrailsstorage

Rails 5.2 RC1 gives attachments their own data model

Rails 5.2's first release candidate includes Active Storage. Its Blob and Attachment models separate file metadata from application records and cloud objects, supporting direct uploads and shared files.

A file attached to an application record has several identities. The application needs to know which record uses it, while a storage service needs a key for its bytes. Its filename and content type are metadata about that file. Active Storage's model keeps these responsibilities separate instead of adding another set of file columns to each application model.

The January 31 Rails 5.2 RC1 announcement makes Active Storage the release's main addition. The framework comes from Basecamp 3, extracted by George Claghorn and David Heinemeier Hansson. RC1 also adds deeper content-type identification after the beta.

This is still a release candidate. The announcement describes a target of another candidate or a final release before the end of February, depending on reported problems. That is a stated plan, not evidence that the final version already exists. The candidate gives application authors a concrete version of the new attachment design to evaluate.

I find Ruby approachable for larger object-oriented programs when the objects have clear responsibilities. Active Storage's distinction between a blob and an attachment fits that preference. The difference remains useful even when a small example begins with only one model and one file.

A blob describes a file

The RC1 Active Storage README describes Blob and Attachment as Active Record-backed models. A Blob holds metadata such as the filename and content type, together with the identifier used by the storage service. It does not hold the file's binary data.

Attachment is the polymorphic join between an application record and a Blob. This lets different application models associate with files without each acquiring storage-specific columns. The application record expresses its attachment relationship, while the shared models record the file metadata and association.

The README treats a blob as immutable in intent. One file corresponds to one blob, and transformations should produce another representation instead of mutating that file's identity in place. The same blob can also associate with more than one application model.

This makes the join model substantive. If a file can be shared, the association between a record and the file is a separate fact from the file itself. Modeling the association explicitly avoids forcing those two facts into one row on the owning application record.

The physical data remains with the configured service. The README lists cloud services and also a disk service for testing or local deployment. A database row therefore describes where a file is and how the application refers to it, while the service holds the bytes.

The application declares its association

The candidate's installation task copies the Active Storage migrations.

rails active_storage:install

A model can then declare one attached file through the documented association API. This example follows the candidate README, with a JPEG content type.

class User < ApplicationRecord
  has_one_attached :avatar
end

user.avatar.attach(
  io: File.open("/path/to/face.jpg"),
  filename: "face.jpg",
  content_type: "image/jpeg"
)

The declaration does not require additional avatar metadata columns on User. Active Storage's own models carry that information. The user-facing association gives application code access to the relationship without making the file's cloud key an ordinary User attribute.

For several attachments, the candidate provides has_many_attached. Its README also shows the with_attached_images scope for loading a collection of messages with their image relationships. This is a reminder that attachment abstraction still has database access behavior. A convenient association can otherwise lead to repeated lookups when rendered across many records.

The distinction between a single attachment and a collection belongs in the model declaration. It gives application code a named relationship while retaining the common Blob and Attachment implementation underneath.

Direct upload changes the route taken by bytes

Active Storage supports uploading through the application server or directly from the client to the cloud. The candidate includes JavaScript for the direct path. A file input marked for direct upload starts that workflow on form submission.

The documented events expose two separate requests. One obtains direct-upload metadata from the application, while another sends the file to storage. Progress and error events let the application describe the upload's state to the user.

This means a direct upload does not eliminate the application's role in describing the blob. It changes which connection carries the file data. The event model distinguishes requesting metadata from storing bytes, consistent with the database model's separation of description and physical content.

The announcement names support for Amazon S3, Google Cloud Storage and Microsoft Azure storage. A common attachment API covers those services, but the objects still live in the selected service. The abstraction centralizes the association and access patterns inside Rails.

A public attachment URL adds indirection

The candidate README describes an application URL for a blob that redirects to the actual service endpoint when accessed. That indirection separates the URL the application publishes from the current storage endpoint. It also supports the arrangement of a main service with mirrors.

The README gives the redirect an HTTP expiration of five minutes. The important design point is that the application URL and the storage location have different roles. A public reference need not encode every detail of where the file currently resides.

Images can have on-demand variants, and the announcement also describes previews for videos and PDFs. Metadata analysis can happen asynchronously. These operations attach processing behavior to the stored file representation without making the application model implement every media-specific step.

My preference is for the framework to make the persistence consequences visible. The candidate documents synchronous purge, which destroys the related models and resource files, and purge_later, which performs that work through Active Job. Deleting attachment data therefore involves both database state and stored resources.

RC1 includes more than attachment syntax

The release candidate also carries encrypted credentials and a Redis cache store. The announcement describes cache failures as misses and discusses key recycling and compression. Those are separate application facilities, while Active Storage supplies the new file-association model.

For attachment users, the substantive change is a standard place for metadata and relationships. Blob identifies the file, Attachment records which model uses it, and the storage service retains its bytes. Direct upload and redirect URLs use those distinctions to support a cloud-oriented workflow through a common Rails interface.