You want to keep some data but do not want to create the new column in the database? Your data should be stored in a form different from the one the user got to deal with?

The virtual attributes could help you! Let’s look at the main cases that come to our aid.

Photo by Bram Van Oost on Unsplash

What is ‘Virtual Attribute’?

The Virtual Attribute is a class attribute, which has no representation in the database. It becomes available after object initialization and remains alive while the object itself is available (like the instance methods). We defined some cases, where virtual attributes are useful and make the code and development more understandable.

Let’s check out how we use them in code samples below.

Simple feature flag

For example, after submitting the form, the attribute has a value (a checkbox was marked). Depending on it, the specific scenario should be chosen. In other words, the virtual attribute could be used for running a hook.

class Post < ActiveRecord::Base

attr_accessor :simple_feature_flag before_validation if: :simple_feature_flag do

self.archived = true

end

end

This case has a feature — the virtual attribute can take any type of value.

Supporting the multiple types for the same variable could lead to a code smell. In Rails 5, ActiveRecord has an attribute class method. This method makes virtual attribute look like persistent model attribute, defines the type and allows being under dirty tracking.

class Post < ActiveRecord::Base

attribute :virtual_attribute_with_type_cast, :boolean

end # Tests

class VirtualAttributesUseCase < Minitest::Test

def test_attribute_api

post = Post.new virtual_attribute_with_type_cast: '1'

assert_equal(

true,

post.virtual_attribute_with_type_cast,

'type casting works'

)

assert(

post.virtual_attribute_with_type_cast_changed?,

'dirty module has been enabled'

)

end

end

Decorator for storing the composite values

One of the most common usages of virtual attributes is decorating several model attributes in one parameter and splitting it back as it was originally used in the database. For example, the virtual attribute we use in the code sample below — author_full_name is stored as 2 separate columns in the DB table — author_first_name and author_last_name, but here and now — we use these 2 columns together in author_full_name.

class Post < ActiveRecord::Base

def author_full_name

[author_first_name, author_last_name].join(' ')

end def author_full_name=(name)

split = name.split(' ', 2)

self.author_first_name = split.first

self.author_last_name = split.last

end

end

# Test

class VirtualAttributesUseCase < Minitest::Test

def test_virtual_attribute_with_custom_processing

post = Post.create! author_full_name: 'John Doe'

assert_equal 'John', post.author_first_name

assert_equal 'Doe', post.author_last_name post = Post.create!(

author_first_name: 'John',

author_last_name: 'Doe'

)

assert_equal 'John Doe', post.author_full_name

end

end

The transition from one data type to another, and vice versa

Let’s reproduce the situation when both user and a database are dealing with one format (e.g JSON) and business logic is dealing with another one (e.g. Hash).

Ideally, we need to register a new data type and add a virtual attribute of this type. In this way, it will take over all the work on the data: validation, transformation, and saving.

In our case let’s try to avoid the creation of new data type.

class Post < ActiveRecord::Base

# input data should come in JSON format

attribute :metadata_raw_json # transform to Hash format

serialize :metadata, Hash

...

Let’s take a look at the conversion between data, which user sent from the form, and data, which could be stored in the database.

...

def metadata_raw_json=(value)

# define instance variable to validate new data

@metadata_raw_json = value self.metadata = build_metadata_from(@metadata_raw_json)

end private # returns parsed data or nil

def build_metadata_from(json)

try_parse_metadata!(json) rescue JSON :: ParserError

nil

end # raise JSON::ParserError

def try_parse_metadata!(value)

ActiveSupport::JSON.decode(value)

end

...

When the user entered an invalid parameter, the application should not save an object. The user should get back the error with the invalid parameter to be fixed. Important: the validation errors should be associated with attribute the user deals with ( e.g. metadata_raw_json we used here).

...

validate: metadata_raw_json_valid_format, if: -> {

@metadata_raw_json.present?

} private def metadata_raw_json_valid_format

errors.add(:metadata_raw_json, 'invalid JSON') unless

json(@metadata_raw_json)

end def json(raw_json)

try_parse_metadata!(raw_json) rescue JSON::ParserError

false

end

...

Let’s play the scenario when data is rendered for a user. There are two options:

data which is taken from the user (for example invalid data)

data which is taken from the database (e.g. the first form rendering)

... # returns new data or data which stores in DB

def metadata_raw_json

@metadata_raw_json || metadata.to_json

end

...

Conclusion

The virtual attribute is a good helper when you need a quick and easy solution creating the form fields which are not related to the database. They allow having more flexibility in user interface customizations. The great thing is that the developer is not limited to attributes backed by a database, but he can use virtual attributes as well.