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!