Use timestamp type attribute for predicates in Rails
- Why use timestamp field instead of boolean for checkbox
- Avoid code duplicating with a universal solution
- Conclusion
Why use timestamp field instead of boolean for checkbox
You are about to add a new boolean attribute into your model inside a Ruby on Rails application. It’s simple as is - add a boolean column in DB and call it a day. But stop and think - consider a timestamp type for the corresponding columns in the database.
This approach allows for tracking the time when the value change. That can be helpful in debugging, especially when dealing with production issues.
Ok, it’s preferable to use the timestamp field type on the back-end. But it is still convenient to have a checkbox on the user interface. Thus, the timestamp column type does not match the UI field type (checkbox). That means the value (such as true/false, yes/no, or any other) from the front-end should be transformed into a timestamp or nil
on the back-end. That’s needed, to ensure compatibility with the mass-assignment methods (.new
or #update
).
Avoid code duplicating with a universal solution
One could write this type of transformation logic ad-hoc inside controllers. But you can create a universal solution. One way to achieve this is by defining a helper method in the base model class. In Rails, the base model class is typically ApplicationRecord
. By doing this, you can ensure its availability across all models in the application. See the following example how to do this:
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
# Defines a predicate method and a setter method for the specified attributes.
#
# To use this code in the User model, call the following:
# timestamp_as_boolean :muted
#
# In this scenario, it assumes that a "muted_at" column is defined in the table.
# After calling the code, the following methods are defined on the User model:
# muted? - checks if a user is muted
# muted=(value) - sets the muted_at timestamp if the value is equivalent to boolean "true"
def self.timestamp_as_boolean(*fields)
fields.each do |field|
timestamp_field = "#{field}_at"
predicate = "#{field}?"
define_method(predicate) do
public_send(timestamp_field).present?
end
define_method("#{field}=") do |value|
new_value = ActiveModel::Type::Boolean.new.cast(value) ? Time.current : nil
public_send("#{timestamp_field}=", new_value) if new_value.nil? || !public_send(predicate)
end
end
end
end
We have now implemented a universal solution using some meta-programming techniques in Ruby. Here’s how you can use it. Let’s take the example of the User
model and convert the muted_at
column into a boolean attribute using the following approach:
class User < ApplicationRecord
timestamp_as_boolean :muted
end
To utilize this solution in an ERB template on the user interface, follow these steps:
<%= form.check_box :muted, { checked: @user.muted? } %>
Let’s verify its functionality:
> user = User.last
> user.muted? # => true
> user.update!(muted: false)
> user.muted? # => false
> user.update!(muted: true) # => true
> user.muted? # => true
> user.muted_at # => Thu, 15 Jun 2023 15:16:54.582848000 UTC +00:00
> user.update!(muted: true) # => true
> user.muted_at # => Thu, 15 Jun 2023 15:16:54.582848000 UTC +00:00
It is important to note that after the second assignment of muted = true, the timestamp did not change. This behavior is expected and logical because the value itself was not modified. The purpose of tracking the timestamp is to capture the moment when a change in the value occurs. In this case, since the value remains the same, there is no need for the timestamp to be updated. This behavior aligns with the intended functionality. It ensures that the timestamp accurately reflects when a change in the value of the attribute occurs.
Conclusion
Use a timestamp type for columns in the database. Create a universal solution to transform boolean values. As a result, enhance the functionality and debugging capabilities of our models. The timestamp type allows us to track the values change time. That can aid in identifying and troubleshooting issues, particularly in production environments. Avoid code duplication and ensure consistent behavior across models with a universal solution. Metaprogramming can help with that. These improvements contribute to a more robust and efficient application.
Happy coding!