Last active
January 16, 2024 09:37
-
-
Save ramonrails/e3ae6ac269ca7a28bb2ee21eed824e91 to your computer and use it in GitHub Desktop.
Resilient, faster and maintainable seeding in ruby on rails
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
# a simple user factory | |
# factories/users.rb | |
# | |
FactoryBot.define do | |
factory :user do | |
name { Faker::Name.unique.name } | |
email { Faker::Internet.email(domain: "bharat.in") } | |
# some users will randomly be "admin" | |
role { "admin" } if [true, false].sample | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
# This file should ensure the existence of records required to run the application in every environment (production, | |
# development, test). The code here should be idempotent so that it can be executed at any point in every environment. | |
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). | |
# | |
# Example: | |
# | |
# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| | |
# MovieGenre.find_or_create_by!(name: genre_name) | |
# end | |
# fail-fast with guard-clause | |
# | |
return if Rails.env.production? | |
# | |
# start seeding when not in production | |
# | |
require "factory_bot" | |
require_relative "seeds/helper" | |
# seed constants | |
seeds = { | |
alarms: 10, | |
devices: 10, | |
groups: 5, | |
locations: 5, | |
messages: 10, | |
users: 5, | |
video_calls: 20 | |
} | |
# start a block that will reset the logger on any error | |
# | |
begin | |
# seeds/helper.rb | |
# set the logger to what we need for the current environment | |
# | |
set_logger | |
logrun before: "Seeding the database..." | |
# | |
# Users, that will be Admins | |
# | |
logrun after: "Added #{User.count} users: #{User.random(2).pluck(:email).join(", ")}..." do | |
logrun(before: "Removing existing users...") { User.delete_all } | |
# creates a randomly sized array of hashes, filled with attributes from factory | |
users = Array.new(rand(1..seeds[:users])) { FactoryBot.attributes_for(:user).except(:password, :password_confirmation) } | |
User.insert_all(users) # bulk insert | |
# add at least one known admin | |
FactoryBot.create(:user, role: "admin", name: "Admin", email: "[email protected]") | |
end | |
# fetch data for further use | |
user_ids = User.ids | |
# | |
# Devices | |
# | |
logrun after: "Added #{Device.count} devices: #{Device.random(2).pluck(:name).join(", ")}..." do | |
logrun(before: "Removing existing devices...") { Device.delete_all } | |
devices = Array.new(rand(1..seeds[:devices])) { FactoryBot.attributes_for(:device) } | |
Device.insert_all(devices) | |
end | |
# fetch data for further use | |
device_ids = Device.ids | |
# | |
# Alarms | |
# | |
logrun after: "Added #{Alarm.count} alarms..." do | |
logrun(before: "Removing existing alarms...") { Alarm.delete_all } | |
alarms = Array.new(rand(1..seeds[:alarms])) { FactoryBot.attributes_for(:alarm, user_id: user_ids.sample) } | |
Alarm.insert_all(alarms) | |
end | |
# | |
# Messages | |
# | |
logrun after: "Added #{Message.count} messages... #{Message.random.content}..." do | |
logrun(before: "Removing existing messages...") { Message.delete_all } | |
messages = Array.new(rand(1..seeds[:messages])) { FactoryBot.attributes_for(:message) } | |
Message.insert_all(messages) | |
end | |
# | |
# Video Calls | |
# | |
logrun after: "Added #{VideoCall.count} video_calls..." do | |
logrun(before: "Removing existing video_calls...") { VideoCall.delete_all } | |
video_calls = Array.new(rand(1..seeds[:video_calls])) do | |
device_id_1, device_id_2 = device_ids.sample(2) | |
FactoryBot.attributes_for(:video_call, source_id: device_id_1, target_id: device_id_2) | |
end | |
VideoCall.insert_all(video_calls) | |
end | |
# | |
# Locations | |
# | |
logrun after: "Added #{Location.count} locations..." do | |
logrun(before: "Removing existing locations...") { Location.delete_all } | |
locations_array = Array.new(rand(1..seeds[:locations])) { FactoryBot.attributes_for(:location) } | |
Location.insert_all(locations_array) | |
end | |
# fetch ids for further use | |
location_ids = Location.ids | |
# | |
# Groups => Devices and Locations must be created before this | |
# | |
logrun after: "Added #{Group.count} groups..." do | |
logrun(before: "Removing existing groups...") { Group.delete_all } | |
rand(1..seeds[:groups]).times do | |
group_with_devices(devices_count: rand(2..5), location_ids: location_ids) | |
end | |
end | |
# rescue from any exception as StandardError | |
# we need this to reset the logger back to original | |
# | |
rescue => e | |
# | |
# switch the logger back to what was earlier | |
reset_logger | |
# | |
# raise the exception again to get rescued in the call chain | |
raise e | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
# | |
# helper methods for seeding | |
# | |
# | |
# Log and Run the block | |
# | |
# @param [String] message to log in the currently set logger | |
# [Block] block to yield | |
# | |
def logrun(**message) | |
# show this message --before-- executing the block | |
Rails.logger.info message[:before].to_s if message[:before] | |
# run the block if given | |
yield if block_given? | |
# show this message --after-- executing the block | |
Rails.logger.info message[:after].to_s if message[:after] | |
end | |
# | |
# switch the logger to $stdout and use `logger.info` for status updates | |
# | |
# @return [Object] Logger object that we have set | |
# | |
def set_logger | |
# remember the existing logger | |
@pre_seed_logger = Rails.logger | |
# output to STDOUT only for development environment | |
Rails.env.development? && Rails.logger = Logger.new($stdout) | |
# example: ELK LogStash in production | |
# Rails.env.production? && Rails.logger = LogStashLogger.new(type: :udp, host: 'localhost', port: 5044) | |
end | |
# | |
# Reset the logger back to what it was before seeding | |
# | |
def reset_logger | |
# reset back to pre-seed logger | |
Rails.logger = @pre_seed_logger | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment