diff --git a/.env.example b/.env.example
new file mode 100644
index 00000000..5ec60dd7
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,11 @@
+DB_USERNAME=postgres
+DB_PASSWORD=postgres
+DB_HOST=db
+DB_PORT=5432
+DB_POOL=5
+DB_NAME=optimization_3
+
+PGHERO_USERNAME=postgres
+PGHERO_PASSWORD=postgres
+
+RAILS_ENV=development
diff --git a/.gitignore b/.gitignore
index 59c74047..40188e3b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,7 @@
/.bundle
+/.idea
/tmp
/log
/public
+.env
+fixtures/1M.json
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..a7ac5b35
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,14 @@
+FROM ruby:2.6.3-alpine
+
+RUN apk update && apk upgrade && apk add --update --no-cache \
+ build-base libc-dev tzdata bash htop shared-mime-info \
+ postgresql-dev postgresql-client
+
+WORKDIR /opt/app
+
+COPY Gemfile* ./
+
+RUN gem install bundler -v 2.0.2
+RUN bundle install
+
+COPY . .
diff --git a/Gemfile b/Gemfile
index e20b1260..eacc035e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -3,10 +3,21 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '2.6.3'
+gem 'activerecord-import'
+gem 'dotenv-rails'
gem 'rails', '~> 5.2.3'
gem 'pg', '>= 0.18', '< 2.0'
gem 'puma', '~> 3.11'
gem 'bootsnap', '>= 1.1.0', require: false
+gem 'mimemagic', '0.3.10'
+gem 'memory_profiler'
+gem 'rspec'
+gem 'rspec-benchmark'
+gem 'ruby-prof'
+gem 'stackprof'
+gem 'pghero', '>= 2'
+gem 'rack-mini-profiler', require: false
+gem 'bullet'
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
diff --git a/Gemfile.lock b/Gemfile.lock
index fccf6f5f..074747d9 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -33,6 +33,8 @@ GEM
activemodel (= 5.2.3)
activesupport (= 5.2.3)
arel (>= 9.0)
+ activerecord-import (1.4.1)
+ activerecord (>= 4.2)
activestorage (5.2.3)
actionpack (= 5.2.3)
activerecord (= 5.2.3)
@@ -43,13 +45,24 @@ GEM
minitest (~> 5.1)
tzinfo (~> 1.1)
arel (9.0.0)
+ benchmark-malloc (0.2.0)
+ benchmark-perf (0.6.0)
+ benchmark-trend (0.4.0)
bindex (0.6.0)
bootsnap (1.4.2)
msgpack (~> 1.0)
builder (3.2.3)
+ bullet (7.0.7)
+ activesupport (>= 3.0.0)
+ uniform_notifier (~> 1.11)
byebug (11.0.1)
concurrent-ruby (1.1.5)
crass (1.0.4)
+ diff-lcs (1.5.0)
+ dotenv (2.8.1)
+ dotenv-rails (2.8.1)
+ dotenv (= 2.8.1)
+ railties (>= 3.2)
erubi (1.8.0)
ffi (1.10.0)
globalid (0.4.2)
@@ -67,8 +80,11 @@ GEM
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
+ memory_profiler (1.0.1)
method_source (0.9.2)
- mimemagic (0.3.3)
+ mimemagic (0.3.10)
+ nokogiri (~> 1)
+ rake
mini_mime (1.0.1)
mini_portile2 (2.4.0)
minitest (5.11.3)
@@ -77,8 +93,12 @@ GEM
nokogiri (1.10.2)
mini_portile2 (~> 2.4.0)
pg (1.1.4)
+ pghero (2.8.3)
+ activerecord (>= 5)
puma (3.12.1)
rack (2.0.6)
+ rack-mini-profiler (3.1.0)
+ rack (>= 1.2.0)
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (5.2.3)
@@ -109,6 +129,25 @@ GEM
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
ffi (~> 1.0)
+ rspec (3.12.0)
+ rspec-core (~> 3.12.0)
+ rspec-expectations (~> 3.12.0)
+ rspec-mocks (~> 3.12.0)
+ rspec-benchmark (0.6.0)
+ benchmark-malloc (~> 0.2)
+ benchmark-perf (~> 0.6)
+ benchmark-trend (~> 0.4)
+ rspec (>= 3.0)
+ rspec-core (3.12.1)
+ rspec-support (~> 3.12.0)
+ rspec-expectations (3.12.2)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.12.0)
+ rspec-mocks (3.12.5)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.12.0)
+ rspec-support (3.12.0)
+ ruby-prof (1.4.3)
ruby_dep (1.5.0)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
@@ -117,10 +156,12 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
+ stackprof (0.2.24)
thor (0.20.3)
thread_safe (0.3.6)
tzinfo (1.2.5)
thread_safe (~> 0.1)
+ uniform_notifier (1.16.0)
web-console (3.7.0)
actionview (>= 5.0)
activemodel (>= 5.0)
@@ -134,12 +175,23 @@ PLATFORMS
ruby
DEPENDENCIES
+ activerecord-import
bootsnap (>= 1.1.0)
+ bullet
byebug
+ dotenv-rails
listen (>= 3.0.5, < 3.2)
+ memory_profiler
+ mimemagic (= 0.3.10)
pg (>= 0.18, < 2.0)
+ pghero (>= 2)
puma (~> 3.11)
+ rack-mini-profiler
rails (~> 5.2.3)
+ rspec
+ rspec-benchmark
+ ruby-prof
+ stackprof
tzinfo-data
web-console (>= 3.3.0)
diff --git a/app/controllers/trips_controller.rb b/app/controllers/trips_controller.rb
index acb38be2..fd45b1a0 100644
--- a/app/controllers/trips_controller.rb
+++ b/app/controllers/trips_controller.rb
@@ -1,7 +1,29 @@
class TripsController < ApplicationController
def index
- @from = City.find_by_name!(params[:from])
- @to = City.find_by_name!(params[:to])
- @trips = Trip.where(from: @from, to: @to).order(:start_time)
+ @from = params[:from]
+ @to = params[:to]
+ @trips_count = trips_query.count
+ @trips = trips_query.order(:start_time)
+ .joins(bus: :services)
+ .select(trips_select.join(','))
+ .group('trips.id, buses.id')
+ end
+
+ private
+
+ def trips_query
+ @cities ||= City.where(name: [@from, @to]).pluck(:name, :id).to_h
+ Trip.where(from_id: @cities[@from], to_id: @cities[@to])
+ end
+
+ def trips_select
+ [
+ 'trips.start_time',
+ 'trips.duration_minutes',
+ 'trips.price_cents',
+ 'array_agg(services.name) as service_names',
+ 'buses.number as bus_number',
+ 'buses.model as bus_model'
+ ]
end
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 10a4cba8..767a072b 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
diff --git a/app/models/bus.rb b/app/models/bus.rb
index 1dcc54cb..0e5b3cc5 100644
--- a/app/models/bus.rb
+++ b/app/models/bus.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
class Bus < ApplicationRecord
MODELS = [
'Икарус',
diff --git a/app/models/city.rb b/app/models/city.rb
index 19ec7f36..dc8306cc 100644
--- a/app/models/city.rb
+++ b/app/models/city.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
class City < ApplicationRecord
validates :name, presence: true, uniqueness: true
validate :name_has_no_spaces
diff --git a/app/models/service.rb b/app/models/service.rb
index 9cbb2a32..23c6aca2 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
class Service < ApplicationRecord
SERVICES = [
'WiFi',
diff --git a/app/models/trip.rb b/app/models/trip.rb
index 9d63dfff..347446b0 100644
--- a/app/models/trip.rb
+++ b/app/models/trip.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
class Trip < ApplicationRecord
- HHMM_REGEXP = /([0-1][0-9]|[2][0-3]):[0-5][0-9]/
+ HHMM_REGEXP = /([0-1][0-9]|[2][0-3]):[0-5][0-9]/.freeze
belongs_to :from, class_name: 'City'
belongs_to :to, class_name: 'City'
diff --git a/app/views/trips/_delimiter.html.erb b/app/views/trips/_delimiter.html.erb
deleted file mode 100644
index 3f845ad0..00000000
--- a/app/views/trips/_delimiter.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-====================================================
diff --git a/app/views/trips/_service.html.erb b/app/views/trips/_service.html.erb
deleted file mode 100644
index 178ea8c0..00000000
--- a/app/views/trips/_service.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-
<%= "#{service.name}" %>
diff --git a/app/views/trips/_services.html.erb b/app/views/trips/_services.html.erb
deleted file mode 100644
index 2de639fc..00000000
--- a/app/views/trips/_services.html.erb
+++ /dev/null
@@ -1,6 +0,0 @@
-Сервисы в автобусе:
-
- <% services.each do |service| %>
- <%= render "service", service: service %>
- <% end %>
-
diff --git a/app/views/trips/_trip.html.erb b/app/views/trips/_trip.html.erb
deleted file mode 100644
index fa1de9aa..00000000
--- a/app/views/trips/_trip.html.erb
+++ /dev/null
@@ -1,5 +0,0 @@
-<%= "Отправление: #{trip.start_time}" %>
-<%= "Прибытие: #{(Time.parse(trip.start_time) + trip.duration_minutes.minutes).strftime('%H:%M')}" %>
-<%= "В пути: #{trip.duration_minutes / 60}ч. #{trip.duration_minutes % 60}мин." %>
-<%= "Цена: #{trip.price_cents / 100}р. #{trip.price_cents % 100}коп." %>
-<%= "Автобус: #{trip.bus.model} №#{trip.bus.number}" %>
diff --git a/app/views/trips/index.html.erb b/app/views/trips/index.html.erb
index a60bce41..2c7b0d6c 100644
--- a/app/views/trips/index.html.erb
+++ b/app/views/trips/index.html.erb
@@ -1,16 +1,26 @@
- <%= "Автобусы #{@from.name} – #{@to.name}" %>
+ <%= "Автобусы #{@from} – #{@to}" %>
- <%= "В расписании #{@trips.count} рейсов" %>
+ <%= "В расписании #{@trips_count} рейсов" %>
<% @trips.each do |trip| %>
- <%= render "trip", trip: trip %>
- <% if trip.bus.services.present? %>
- <%= render "services", services: trip.bus.services %>
+ - <%= "Отправление: #{trip.start_time}" %>
+ - <%= "Прибытие: #{(Time.parse(trip.start_time) + trip.duration_minutes.minutes).strftime('%H:%M')}" %>
+ - <%= "В пути: #{trip.duration_minutes / 60}ч. #{trip.duration_minutes % 60}мин." %>
+ - <%= "Цена: #{trip.price_cents / 100}р. #{trip.price_cents % 100}коп." %>
+ - <%= "Автобус: #{trip.bus_model} №#{trip.bus_number}" %>
+
+ <% if trip.service_names.present? %>
+ - Сервисы в автобусе:
+
+ <% trip.service_names.each do |service| %>
+ - <%= "#{service}" %>
+ <% end %>
+
<% end %>
- <%= render "delimiter" %>
+ ====================================================
<% end %>
diff --git a/case-study.md b/case-study.md
new file mode 100644
index 00000000..01366282
--- /dev/null
+++ b/case-study.md
@@ -0,0 +1,93 @@
+Первым шагом при выполнении задания была настройка запуска в докер и проверка корректности работы.
+После первого запуска скрипта на экспорт данных было заметно что он работает не очень быстро.
+
+### 1 часть задания. Оптимизация скрипта
+
+Скрипт решила оптимизировать первоначально по памяти, т.к. имеет место обработка файла + active record наверняка может создавать лишние объекты
+ну и хотелось в этом больше потренироваться.
+Как и ожидалось после запуска memoryprofiler больше всего памяти приходится на activerecord, поэтому так же как и во втором
+задании поэтапно начинаю оптимизацию.
+
+#### Замеры до оптимизации:
+- Время выполнения (fixtures/small.json): 6.97 сек
+- Потребляемая память (fixtures/small.json): 119 MB
+
+Далее перечислю основные моменты:
+1. Первое что решаю сделать это добавить frozen_string_literal, что дает небольшое улучшение.
+2. Далее, исходя из отчета самое большое количество памяти выделено на /activerecord-5.2.3/lib/active_record/log_subscriber.rb
+Заменила log_level на error для скрипта. Здесь же убрала bootsnap, т.к. периодически падал профилировщик. Точка роста сместилась
+3. Т.к далее основная нагрузка приходится на active record, решаю по максимуму убрать использование объектов active record.
+Для этого выношу код в отдельный класс, заменяю запросы удаления на truncate, подключаю гем activerecord-import и с помощью него уже строю импорт данных
+В результате время выполнения сократилась, но потребление памяти не очень. На первом месте остались объекты active record
+4. Далее решила в импорте не использовать recursive, а собрать все в массив и импортировать его и это уже принесло хороший результат:
+- Время выполнения (fixtures/small.json): 0.81 сек
+- Потребляемая память (fixtures/small.json): 113 MB
+
+
+- Время выполнения (fixtures/medium.json): 4.07 сек
+- Потребляемая память (fixtures/small.json): 112 MB
+
+
+- Время выполнения (fixtures/large.json): 34.86 сек
+- Потребляемая память (fixtures/small.json): 335 MB
+
+В результате оптимизации active record перестал быть главной точкой роста.
+
+5. После профилирования по времени на первом месте оказался PG::Connection#async_exec, судя по записанным данным самое большое количество запросов
+приходится на добавление связи bus и service. Ради интереса решила попробовать сделать их запись в одном запросе, что
+привело к снижению времени обработки. После этого решила попробовать по аналогии записывать и данные по trip и в итоге получила
+значительное снижение по времени, т.о от гема activerecord-import отказалась. Результаты:
+
+- Время выполнения (fixtures/small.json): 0.39 сек
+- Потребляемая память (fixtures/small.json): 110 MB
+
+
+- Время выполнения (fixtures/medium.json): 1.5 сек
+- Потребляемая память (fixtures/medium.json): 116 MB
+
+
+- Время выполнения (fixtures/large.json): 13.28 сек
+- Потребляемая память (fixtures/large.json): 330 MB
+
+
+- Время выполнения (fixtures/1M.json): 137.6 сек
+- Потребляемая память (fixtures/1M.json): 1895 MB
+
+По памяти много, но по времени кажется неплохо. На этом результате решила остановиться пока, потому что времени катастрофически не хватает.
+Потоковая обработка интересна, если получится как нибудь попробую предложенный пример записи в БД.
+
+### 2 часть задания. Оптимизация страницы расписаний
+
+После импорта файла large.json страница загружалась примерно 24 секунды.
+Первым делом решила попробовать поставить pgHero, т.к. было интересно попробовать этот инструмент.
+
+Сразу после первого прогона был найден медленный запрос:
+```sql
+SELECT "services".* FROM "services" INNER JOIN "buses_services" ON "services"."id" = "buses_services"."service_id" WHERE "buses_services"."bus_id" = $1
+```
+
+Поставила bullet и rack-mini-profiler.
+- Далее поправила запросы исходя из алертов и добавила .
+- из профайлера видно что очень много запросов вида:
+```sql
+SELECT "services".* FROM "services" INNER JOIN "buses_services" ON "services"."id" = "buses_services"."service_id" WHERE "buses_services"."bus_id" = $1;
+```
+то же показывает и pgHero, поэтому поправила этот момент тем то изменила выборку по trips - добавила в select только нужные для
+отображения аттрибуты и добавила joins.
+- объединила рендеры в один для удобства поиска необходимых аттрибутов
+- заменила поиск города по названию (влияет не сильно но тем не менее)
+
+В результате время отображения на сократилось до 182ms. По pgHero и bullet проблем больше не выявлено.
+
+Дальше я решила посмотреть анализ самого долгого запроса, который стал основным.
+По нему получается что самым долгим является часть джойна с buses_services, т.к там нет индексов. Такая же проблема на
+таблицах buses и trips. Добавляю индексы.
+
+В результате время отображения на сократилось до 130ms (с включенным профилировщиком)
+
+### Итоги
+
+Очень понравилось использовать pgHero, действительно удобный и визуально приятный инструмент.
+С bullet и rack-mini-profiler немного уже работала, поэтому они остаются полезными)
+
+P.S Прошу прощения за такую задержку с выполнением дз. Постараюсь наверстать все пропущенное до конца курса.
diff --git a/config/application.rb b/config/application.rb
index 9c331097..ea1a1364 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -10,6 +10,7 @@ module Task4
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 5.2
+ config.log_level = :error
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
diff --git a/config/boot.rb b/config/boot.rb
index b9e460ce..b6915e55 100644
--- a/config/boot.rb
+++ b/config/boot.rb
@@ -1,4 +1,5 @@
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup' # Set up gems listed in the Gemfile.
-require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
+# отключила т.к. падал профилировщик
+# require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
diff --git a/config/database.yml b/config/database.yml
index e116cfa6..11508d48 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -17,13 +17,15 @@
default: &default
adapter: postgresql
encoding: unicode
- # For details on connection pooling, see Rails configuration guide
- # http://guides.rubyonrails.org/configuring.html#database-pooling
- pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ username: <%= ENV.fetch('DB_USERNAME') { 'postgres' } %>
+ password: <%= ENV.fetch('DB_PASSWORD') { 'postgres' } %>
+ host: <%= ENV.fetch('DB_HOST') { 'db' } %>
+ port: <%= ENV.fetch('DB_PORT') { 5432 } %>
+ pool: <%= ENV.fetch('DB_POOL') { 5 } %>
development:
<<: *default
- database: task-4_development
+ database: <%= ENV.fetch('DB_NAME') %>
# The specified database role being used to connect to postgres.
# To create additional roles in postgres see `$ createuser --help`.
@@ -57,7 +59,7 @@ development:
# Do not set this db to the same as development or production.
test:
<<: *default
- database: task-4_test
+ database: optimization_3_test
# As with config/secrets.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
@@ -80,6 +82,4 @@ test:
#
production:
<<: *default
- database: task-4_production
- username: task-4
- password: <%= ENV['TASK-4_DATABASE_PASSWORD'] %>
+ database: task-optimization_3_production
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 1311e3e4..903a8661 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -1,4 +1,13 @@
Rails.application.configure do
+ config.after_initialize do
+ Bullet.enable = true
+ Bullet.alert = true
+ Bullet.bullet_logger = true
+ Bullet.console = true
+ Bullet.rails_logger = true
+ Bullet.add_footer = true
+ end
+
# Settings specified here will take precedence over those in config/application.rb.
# In the development environment your application's code is reloaded on
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 613d8289..1b5e478c 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -51,7 +51,7 @@
# Use the lowest log level to ensure availability of diagnostic information
# when problems arise.
- config.log_level = :debug
+ config.log_level = :error
# Prepend all log lines with the following tags.
config.log_tags = [ :request_id ]
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 0a38fd3c..bc5971ab 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -1,4 +1,10 @@
Rails.application.configure do
+ config.after_initialize do
+ Bullet.enable = true
+ Bullet.bullet_logger = true
+ Bullet.raise = true # raise an error if n+1 query occurs
+ end
+
# Settings specified here will take precedence over those in config/application.rb.
# The test environment is used exclusively to run your application's
diff --git a/config/initializers/rack_mini_profiler.rb b/config/initializers/rack_mini_profiler.rb
new file mode 100644
index 00000000..14e63a30
--- /dev/null
+++ b/config/initializers/rack_mini_profiler.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+if Rails.env.development?
+ require "rack-mini-profiler"
+
+ # The initializer was required late, so initialize it manually.
+ Rack::MiniProfilerRails.initialize!(Rails.application)
+end
diff --git a/config/routes.rb b/config/routes.rb
index a2da6a7b..c60fe546 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,4 +1,5 @@
Rails.application.routes.draw do
+ mount PgHero::Engine, at: 'pghero'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
get "/" => "statistics#index"
get "автобусы/:from/:to" => "trips#index"
diff --git a/db/migrate/20230419131514_enable_pg_statements.rb b/db/migrate/20230419131514_enable_pg_statements.rb
new file mode 100644
index 00000000..d5ec3fa2
--- /dev/null
+++ b/db/migrate/20230419131514_enable_pg_statements.rb
@@ -0,0 +1,5 @@
+class EnablePgStatements < ActiveRecord::Migration[5.2]
+ def change
+ enable_extension 'pg_stat_statements'
+ end
+end
\ No newline at end of file
diff --git a/db/migrate/20230421134615_create_index_on_bus_services.rb b/db/migrate/20230421134615_create_index_on_bus_services.rb
new file mode 100644
index 00000000..5db9ef50
--- /dev/null
+++ b/db/migrate/20230421134615_create_index_on_bus_services.rb
@@ -0,0 +1,6 @@
+class CreateIndexOnBusServices < ActiveRecord::Migration[5.2]
+ def change
+ add_index :buses_services, :bus_id
+ add_index :buses_services, :service_id
+ end
+end
\ No newline at end of file
diff --git a/db/migrate/20230421134619_create_index_on_trips.rb b/db/migrate/20230421134619_create_index_on_trips.rb
new file mode 100644
index 00000000..585cd9df
--- /dev/null
+++ b/db/migrate/20230421134619_create_index_on_trips.rb
@@ -0,0 +1,7 @@
+class CreateIndexOnTrips < ActiveRecord::Migration[5.2]
+ def change
+ add_index :trips, :bus_id
+ add_index :trips, [:from_id, :to_id]
+ add_index :trips, :start_time
+ end
+end
\ No newline at end of file
diff --git a/db/schema.rb b/db/schema.rb
index f6921e45..58f9dab1 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,9 +10,10 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2019_03_30_193044) do
+ActiveRecord::Schema.define(version: 2023_04_21_134619) do
# These are extensions that must be enabled in order to support this database
+ enable_extension "pg_stat_statements"
enable_extension "plpgsql"
create_table "buses", force: :cascade do |t|
@@ -23,6 +24,8 @@
create_table "buses_services", force: :cascade do |t|
t.integer "bus_id"
t.integer "service_id"
+ t.index ["bus_id"], name: "index_buses_services_on_bus_id"
+ t.index ["service_id"], name: "index_buses_services_on_service_id"
end
create_table "cities", force: :cascade do |t|
@@ -40,6 +43,9 @@
t.integer "duration_minutes"
t.integer "price_cents"
t.integer "bus_id"
+ t.index ["bus_id"], name: "index_trips_on_bus_id"
+ t.index ["from_id", "to_id"], name: "index_trips_on_from_id_and_to_id"
+ t.index ["start_time"], name: "index_trips_on_start_time"
end
end
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..4e7e63f1
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,39 @@
+version: '3.4'
+services:
+ db:
+ image: postgres:10
+ environment:
+ POSTGRES_USER: $DB_USERNAME
+ POSTGRES_PASSWORD: $DB_PASSWORD
+ ports:
+ - "5432:5432"
+ command:
+ - "postgres"
+ - "-c"
+ - "shared_preload_libraries=pg_stat_statements"
+ - "-c"
+ - "pg_stat_statements.track=all"
+ - "-p"
+ - "5432"
+ volumes:
+ - db:/var/lib/postgresql/data
+ web:
+ tty: true
+ stdin_open: true
+ image: rails-optimization-task3
+ build:
+ context: .
+ command: sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
+ volumes:
+ - .:/opt/app:cached
+# tmpfs:
+# - /opt/app/tmp/
+# - /opt/app/log/
+ ports:
+ - "3000:3000"
+ depends_on:
+ - db
+ environment:
+ RAILS_ENV: $RAILS_ENV
+volumes:
+ db:
diff --git a/lib/operations/import.rb b/lib/operations/import.rb
new file mode 100644
index 00000000..5939a4e4
--- /dev/null
+++ b/lib/operations/import.rb
@@ -0,0 +1,102 @@
+module Operations
+ class Import
+ def initialize
+ @services = {}
+ @cities = {}
+ @first_trips_line = true
+ @first_buses_line = true
+ end
+
+ def truncate_tables!
+ ActiveRecord::Base.connection.execute('truncate table cities, buses, services, trips, buses_services RESTART IDENTITY;')
+ end
+
+ def find_city_id(name)
+ @cities[name] ||= ActiveRecord::Base.connection.execute("insert into cities (name) values ('#{name}') ON CONFLICT DO NOTHING RETURNING id").first['id']
+ end
+
+ def import_services!
+ ActiveRecord::Base.connection.execute("insert into services (name) values ('#{Service::SERVICES.join('\'),(\'')}') RETURNING id, name").each do |result|
+ @services[result['name']] = result['id']
+ end
+ end
+
+ def insert_bus(model:, number:)
+ ActiveRecord::Base.connection.execute("insert into buses (model, number) values ('#{model}', '#{number}') ON CONFLICT DO NOTHING RETURNING id").first['id']
+ end
+
+ def insert_bus_services(bus_id:, service_names:)
+ service_names.map! { |n| "(#{bus_id}, #{find_service_id(n)})" }
+ ActiveRecord::Base.connection.execute("insert into buses_services (bus_id, service_id) values #{service_names.join(',')} ON CONFLICT DO NOTHING")
+ end
+
+ def buses_services_sql_file
+ # bus_id, service_id
+ @buses_services_sql_file ||= File.open('log/buses_services_sql.txt', 'w+')
+ end
+
+ def trips_sql_file
+ # start_time, duration_minutes, price_cents, from_id, to_id, bus_id
+ @trips_sql_file ||= File.open('log/trips_sql.txt', 'w+')
+ end
+
+ def write_trip(bus_id, raw_data)
+ if @first_trips_line
+ trips_sql_file.write 'INSERT INTO trips (start_time, duration_minutes, price_cents, from_id, to_id, bus_id) VALUES '
+ @first_trips_line = false
+ else
+ trips_sql_file.write ','
+ end
+
+ trips_sql_file.write < :environment do |_task, args|
- json = JSON.parse(File.read(args.file_name))
-
- ActiveRecord::Base.transaction do
- City.delete_all
- Bus.delete_all
- Service.delete_all
- Trip.delete_all
- ActiveRecord::Base.connection.execute('delete from buses_services;')
-
- json.each do |trip|
- from = City.find_or_create_by(name: trip['from'])
- to = City.find_or_create_by(name: trip['to'])
- services = []
- trip['bus']['services'].each do |service|
- s = Service.find_or_create_by(name: service)
- services << s
- end
- bus = Bus.find_or_create_by(number: trip['bus']['number'])
- bus.update(model: trip['bus']['model'], services: services)
-
- Trip.create!(
- from: from,
- to: to,
- bus: bus,
- start_time: trip['start_time'],
- duration_minutes: trip['duration_minutes'],
- price_cents: trip['price_cents'],
- )
- end
- end
+ Operations::Import.new.work(args.file_name)
end