diff --git a/day_2/tn_rails_concurrent/Gemfile b/day_2/tn_rails_concurrent/Gemfile index b6d6d35..9a526f1 100644 --- a/day_2/tn_rails_concurrent/Gemfile +++ b/day_2/tn_rails_concurrent/Gemfile @@ -26,3 +26,4 @@ gem "parallel", "~> 1.26.3" gem "sidekiq", "~> 7.3.6" gem "activerecord-import", "~> 2.0" +gem "active_model_serializers" diff --git a/day_2/tn_rails_concurrent/Gemfile.lock b/day_2/tn_rails_concurrent/Gemfile.lock index 1d38d4b..2342760 100644 --- a/day_2/tn_rails_concurrent/Gemfile.lock +++ b/day_2/tn_rails_concurrent/Gemfile.lock @@ -50,6 +50,11 @@ GEM erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) + active_model_serializers (0.10.15) + actionpack (>= 4.1) + activemodel (>= 4.1) + case_transform (>= 0.2) + jsonapi-renderer (>= 0.1.1.beta1, < 0.3) activejob (7.1.5.1) activesupport (= 7.1.5.1) globalid (>= 0.3.6) @@ -90,6 +95,8 @@ GEM bootsnap (1.18.4) msgpack (~> 1.2) builder (3.3.0) + case_transform (0.2) + activesupport concurrent-ruby (1.3.4) connection_pool (2.4.1) crass (1.0.6) @@ -111,6 +118,7 @@ GEM irb (1.14.2) rdoc (>= 4.0.0) reline (>= 0.4.2) + jsonapi-renderer (0.2.2) logger (1.6.3) loofah (2.23.1) crass (~> 1.0.2) @@ -233,6 +241,7 @@ PLATFORMS x86_64-linux DEPENDENCIES + active_model_serializers activerecord-import (~> 2.0) annotate bootsnap diff --git a/day_2/tn_rails_concurrent/app/controllers/orders_controller.rb b/day_2/tn_rails_concurrent/app/controllers/orders_controller.rb index 80cd997..f2189f1 100644 --- a/day_2/tn_rails_concurrent/app/controllers/orders_controller.rb +++ b/day_2/tn_rails_concurrent/app/controllers/orders_controller.rb @@ -1,6 +1,6 @@ class OrdersController < ApplicationController def top_products_report - top_products = Order.all - render json: top_products + top_products = ProductsByOrdersCount.new.call + render json: top_products, each_serializer: TopProductsSerializer end end diff --git a/day_2/tn_rails_concurrent/app/models/order.rb b/day_2/tn_rails_concurrent/app/models/order.rb index efe9965..3b2f06c 100644 --- a/day_2/tn_rails_concurrent/app/models/order.rb +++ b/day_2/tn_rails_concurrent/app/models/order.rb @@ -10,7 +10,7 @@ # updated_at :datetime not null # class Order < ApplicationRecord - belongs_to :product + belongs_to :product, counter_cache: true scope :pending, -> { where(current_status: :pending) } scope :processed, -> { where(current_status: :processed) } diff --git a/day_2/tn_rails_concurrent/app/queries/products_by_orders_count.rb b/day_2/tn_rails_concurrent/app/queries/products_by_orders_count.rb new file mode 100644 index 0000000..c3e51e4 --- /dev/null +++ b/day_2/tn_rails_concurrent/app/queries/products_by_orders_count.rb @@ -0,0 +1,43 @@ +class ProductsByOrdersCount + def initialize(count: 10) + @count = count + end + + def call + sanitized_sql = ActiveRecord::Base.sanitize_sql_array([sql, count, count]) + Product.find_by_sql(sanitized_sql) + end + + private + + attr_reader :count + + def sql + @sql ||= <<-SQL.squish + WITH top_products AS ( + SELECT * + FROM products + ORDER BY orders_count DESC + LIMIT ? + ), + max_days AS ( + SELECT product_id, DATE(created_at) AS order_date, SUM(quantity) AS daily_quantity + FROM orders + WHERE product_id IN (SELECT product_id FROM top_products) + GROUP BY product_id, DATE(created_at) + ), + max_days_per_product AS ( + SELECT product_id, order_date, daily_quantity, + RANK() OVER (PARTITION BY product_id ORDER BY daily_quantity DESC) as rank + FROM max_days + ) + + SELECT t.*, m.order_date as date + FROM max_days_per_product as m + JOIN top_products as t ON t.id = m.product_id + WHERE m.rank = 1 + ORDER BY t.orders_count DESC + LIMIT ?; + SQL + end +end diff --git a/day_2/tn_rails_concurrent/app/serializers/top_products_serializer.rb b/day_2/tn_rails_concurrent/app/serializers/top_products_serializer.rb new file mode 100644 index 0000000..68cec23 --- /dev/null +++ b/day_2/tn_rails_concurrent/app/serializers/top_products_serializer.rb @@ -0,0 +1,7 @@ +class TopProductsSerializer < ActiveModel::Serializer + attributes :product_id, :date, :total_quantity + + def product_id = self.object.id + def total_quantity = self.object.orders_count + def date = self.object.date.strftime("%d-%m-%Y") +end diff --git a/day_2/tn_rails_concurrent/db/migrate/20241227185054_add_orders_count_to_products.rb b/day_2/tn_rails_concurrent/db/migrate/20241227185054_add_orders_count_to_products.rb new file mode 100644 index 0000000..df54c98 --- /dev/null +++ b/day_2/tn_rails_concurrent/db/migrate/20241227185054_add_orders_count_to_products.rb @@ -0,0 +1,5 @@ +class AddOrdersCountToProducts < ActiveRecord::Migration[7.1] + def change + add_column :products, :orders_count, :integer, default: 0, null: false + end +end diff --git a/day_2/tn_rails_concurrent/db/seeds.rb b/day_2/tn_rails_concurrent/db/seeds.rb index 0de8a9b..2dfd6c9 100644 --- a/day_2/tn_rails_concurrent/db/seeds.rb +++ b/day_2/tn_rails_concurrent/db/seeds.rb @@ -3,14 +3,14 @@ Product.destroy_all puts "Products destroyed" -Product.insert_all(Array.new(100) { { name: "Product #{rand(1..1000)}", stock: rand(1..50), price: rand(10..100) } }) +Product.insert_all(Array.new(100_000) { { name: "Product #{rand(1..100_000_000)}", stock: rand(1..50_00), price: rand(10..100_000) } }) puts "Products created: #{Product.count}" product_ids = Product.pluck(:id) statuses = ["pending", "processed", "cancelled"] dates = (1..30).map { |n| n.days.ago } ActiveRecord::Base.record_timestamps = false -Order.insert_all(Array.new(20_000) { +Order.insert_all(Array.new(100_000_000) { { product_id: product_ids.sample, quantity: rand(1..10), current_status: statuses.sample, diff --git a/day_2/tn_rails_concurrent/db/structure.sql b/day_2/tn_rails_concurrent/db/structure.sql index 3e25355..19c67c5 100644 --- a/day_2/tn_rails_concurrent/db/structure.sql +++ b/day_2/tn_rails_concurrent/db/structure.sql @@ -79,7 +79,8 @@ CREATE TABLE public.products ( stock integer, price numeric, created_at timestamp(6) without time zone NOT NULL, - updated_at timestamp(6) without time zone NOT NULL + updated_at timestamp(6) without time zone NOT NULL, + orders_count integer DEFAULT 0 NOT NULL ); @@ -164,6 +165,7 @@ ALTER TABLE ONLY public.schema_migrations SET search_path TO "$user", public; INSERT INTO "schema_migrations" (version) VALUES +('20241227185054'), ('20241216120643'), ('20241216120641');