When I first conceived of this app, it was the summer season, and now it’s autumn, and I need to switch to a smart trainer. This transition involves different configurations since I won’t be using traditional wheels and brakes, and it also pertains to a distinct activity type.
The simplest solution, which doesn’t require any code modifications, is to add components one by one and create different bike configurations. Each component would track its distance, but this wouldn’t contribute to the overall distance of the bike. However, I decided against this solution because I want to track all activities on the trainer as a single bike.
To achieve this, I needed to write some code. My initial idea was to replace a string with an array. Instead of having a single sport_type
, I could have sport_types
as an array. However, since I’m using SQLite, which lacks a native array data type, I would have to use serialized strings to store this array.
Storing data in serialized arrays can have its issues, particularly when querying and searching for specific values within the arrays. But considering I don’t have a large amount of data to manage, I decided to go with this approach.
To make it work with ActiveRecord, I utilized the serialize
method. I modified my bike model and contract as follows:
module Records
class Bike < ActiveRecord::Base
has_many :component_assignments
has_many :components, through: :component_assignments
serialize :sport_types, Array
end
end
end```
```module App
module Contracts
class Bike < Dry::Validation::Contract
params do
required(:name).filled(:string)
optional(:brand).maybe(:string)
optional(:model).maybe(:string)
optional(:weight).maybe(:float)
optional(:notes).maybe(:string)
optional(:commute).maybe(:string)
optional(:sport_types).value(:array).each(:string)
end
end
end
end```
This setup allows querying for activities with a specific sport type easily. For instance:
Records::Activity.where(sport_type: record.sport_types)
However, it doesn't work in the opposite direction—finding all bikes with a specific sport type.
I realized there are potential issues with keeping data as an array, especially when making updates or modifications to individual elements within the array. Large arrays can be challenging to manage as it might require parsing the array, making changes in your application code, and rewriting the entire array back to the database.
An alternative approach to address this issue is to use separate tables. I created two tables: one to store all available sport types and another to assign sport types to bikes. This solution is more complex, as it involves creating new records, endpoints, and additional logic.
class CreateSportTypesTable < ActiveRecord::Migration[7.0] def change create_table :sport_types do |t| t.string :name
t.timestamps
end
end end
class CreateBikeSportTypes < ActiveRecord::Migration[7.0] def change create_table :bike_sport_types do |t| t.references :bike, foreign_key: true t.references :sport_type, foreign_key: true
t.timestamps
end
end end
In this approach, you need to create new records and new endpoints, significantly expanding your application's logic. You'll have new models for these tables, and you'll need to update your queries to consider these changes.
module App module Records class SportType < ActiveRecord::Base has_many :bike_sport_types has_many :bikes, through: :bike_sport_types end end end
module App module Records class BikeSportType < ActiveRecord::Base belongs_to :sport_type belongs_to : bike end end end
To maintain backward compatibility with existing data, you can create a migration script to migrate data from the old sport_type field to the new table.
class AddSportTypeToActivity < ActiveRecord::Migration[7.0] def change rename_column :activities, :sport_type, :old_sport_type add_column :activities, :sport_type_id, :integer
::App::Records::Activity.find_each do |activity|
sport_type = ::App::Records::SportType.find_or create_by(name: activity.old_sport_type)
activity.update(sport_type_id: sport_type.id)
end
remove_column :activities, :sport_type
end end In this approach, you’ll have a better database structure and maintain relationships between activities, bikes, and sport types more effectively.
This change requires you to update your queries to consider the new relationship:
class AddSportTypeToActivity < ActiveRecord::Migration[7.0]
def change
rename_column :activities, :sport_type, :old_sport_type
add_column :activities, :sport_type_id, :integer
::App::Records::Activity.find_each do |activity|
sport_type = ::App::Records::SportType.find_or create_by(name: activity.old_sport_type)
activity.update(sport_type_id: sport_type.id)
end
remove_column :activities, :sport_type
end
end
In this approach, we’ll have a better database structure and maintain relationships between activities, bikes, and sport types more effectively.
This change requires to update our queries to consider the new relationship:
Records::Activity.where(commute: record.commute, sport_type: record.sport_types.map(&:name))
Additionally, I can enhance my bike responses by including the sport_types:
def to_model(record)
Models::Bike.new(
id: record.id,
name: record.name,
brand: record.brand,
model: record.model,
weight: record.weight,
notes: record notes,
commute: record.commute,
sport_types: map_sport_types(record),
distance: calculate_distance(record),
time: calculate_time(record)
)
end
def map_sport_types(record)
record.sport_types.map(&:name) if record.sport_types
end
This approach allows you to manage and query your data more efficiently and enhances the clarity of your code. It may involve some initial complexity, but it can lead to a more maintainable and extensible application in the long run. Code is on github. Stay curious, keep coding, and happy developing!