From ff4acdff4d8967316c2d6d02e259b6058a95ae59 Mon Sep 17 00:00:00 2001 From: hanguyen Date: Tue, 2 May 2023 07:16:07 +0100 Subject: [PATCH] finish shop manager challenge --- app.rb | 21 ++++ items_class_design.md | 201 ++++++++++++++++++++++++++++++++++ lib/app_test.rb | 54 +++++++++ lib/database_connection.rb | 28 +++++ lib/item.rb | 4 + lib/item_repository.rb | 38 +++++++ lib/order.rb | 7 ++ lib/order_repository.rb | 56 ++++++++++ orders_class_design.md | 200 +++++++++++++++++++++++++++++++++ seeds.sql | 23 ++++ spec/app_test_spec.rb | 84 ++++++++++++++ spec/item_repository_spec.rb | 41 +++++++ spec/order_repository_spec.rb | 46 ++++++++ spec/seeds_items.sql | 17 +++ spec/seeds_orders.sql | 24 ++++ spec/spec_helper.rb | 11 +- two_tables_design.md | 157 ++++++++++++++++++++++++++ 17 files changed, 1011 insertions(+), 1 deletion(-) create mode 100644 app.rb create mode 100644 items_class_design.md create mode 100644 lib/app_test.rb create mode 100644 lib/database_connection.rb create mode 100644 lib/item.rb create mode 100644 lib/item_repository.rb create mode 100644 lib/order.rb create mode 100644 lib/order_repository.rb create mode 100644 orders_class_design.md create mode 100644 seeds.sql create mode 100644 spec/app_test_spec.rb create mode 100644 spec/item_repository_spec.rb create mode 100644 spec/order_repository_spec.rb create mode 100644 spec/seeds_items.sql create mode 100644 spec/seeds_orders.sql create mode 100644 two_tables_design.md diff --git a/app.rb b/app.rb new file mode 100644 index 00000000..2e29f3e8 --- /dev/null +++ b/app.rb @@ -0,0 +1,21 @@ +# file: app.rb + +require_relative 'lib/database_connection' +require_relative 'lib/item_repository' +require_relative 'lib/order_repository' +# We need to give the database name to the method `connect`. +DatabaseConnection.connect('items_orders') + +# Perform a SQL query on the database and get the result set. +# sql = 'SELECT id, name, unit_price, quantity FROM items;' +# result = DatabaseConnection.exec_params(sql, []) + +# Print out each record from the result set . +# result.each do |record| +# p record +# end +# repo = ItemRepository.new +# p repo.all +repo = OrderRepository.new +p repo.find_with_items(1) + diff --git a/items_class_design.md b/items_class_design.md new file mode 100644 index 00000000..f041775c --- /dev/null +++ b/items_class_design.md @@ -0,0 +1,201 @@ +# Items Model and Repository Classes Design Recipe + +_Copy this recipe template to design and implement Model and Repository classes for a database table._ + +## 1. Design and create the Table + +If the table is already created in the database, you can skip this step. + +Otherwise, [follow this recipe to design and create the SQL schema for your table](./single_table_design_recipe_template.md). + +_In this template, we'll use an example table `students`_ + +``` +# EXAMPLE + +Table: items + +Columns: + id | name | unit_price | quantity +``` + +## 2. Create Test SQL seeds + +Your tests will depend on data stored in PostgreSQL to run. + +If seed data is provided (or you already created it), you can skip this step. + +```sql +-- EXAMPLE +-- (file: spec/seeds_{table_name}.sql) + +-- Write your SQL seed here. + +-- First, you'd need to truncate the table - this is so our table is emptied between each test run, +-- so we can start with a fresh state. +-- (RESTART IDENTITY resets the primary key) + +TRUNCATE TABLE items RESTART IDENTITY; -- replace with your own table name. + +-- Below this line there should only be `INSERT` statements. +-- Replace these statements with your own seed data. + +INSERT INTO items (name, unit_price, quantity) VALUES ('book', '2', '100'); +INSERT INTO items (name, unit_price, quantity) VALUES ('pen', '1', '200'); +``` + +Run this SQL file on the database to truncate (empty) the table, and insert the seed data. Be mindful of the fact any existing records in the table will be deleted. + +```bash +psql -h 127.0.0.1 items_orders_test < spec/seeds_items.sql +``` + +## 3. Define the class names + +Usually, the Model class name will be the capitalised table name (single instead of plural). The same name is then suffixed by `Repository` for the Repository class name. + +```ruby +# EXAMPLE +# Table name: students + +# Model class +# (in lib/item.rb) +class Items +end + +# Repository class +# (in lib/item_repository.rb) +class ItemRepository +end +``` + +## 4. Implement the Model class + +Define the attributes of your Model class. You can usually map the table columns to the attributes of the class, including primary and foreign keys. + +````ruby +# EXAMPLE +# Table name: students + +# Model class +# (in lib/item.rb) + +class Item + attr_accessor :id, :name, :unit_price,:quantity +end + + + + +_You may choose to test-drive this class, but unless it contains any more logic than the example above, it is probably not needed._ + +## 5. Define the Repository Class interface + +Your Repository class will need to implement methods for each "read" or "write" operation you'd like to run against the database. + +Using comments, define the method signatures (arguments and return value) and what they do - write up the SQL queries that will be used by each method. + +```ruby +# EXAMPLE +# Table name: items + +# Repository class +# (in lib/item_repository.rb) + +class ItemRepository + + # Selecting all items + # No arguments + def all + # Executes the SQL query: + # SELECT id, name, unit_price, quantity FROM items; + + # Returns an array of Item objects. + end + + # Gets a single record by its ID + # One argument: the id (number) + def find(id) + # Executes the SQL query: + # SELECT id, name, cohort_name FROM students WHERE id = $1; + + # Returns a single Student object. + end + + # Add more methods below for each operation you'd like to implement. + + # def create(student) + # end + + # def update(student) + # end + + # def delete(student) + # end +end +```` + +## 6. Write Test Examples + +Write Ruby code that defines the expected behaviour of the Repository class, following your design from the table written in step 5. + +These examples will later be encoded as RSpec tests. + +```ruby +# EXAMPLES + +# 1 +# Get all students + +repo = ItemRepository.new + +items = repo.all + +items.length # => 2 +items.first.name # => "book" + +# 2 +# Get a single student + +repo = StudentRepository.new + +student = repo.find(1) + +student.id # => 1 +student.name # => 'David' +student.cohort_name # => 'April 2022' + +# Add more examples for each method +``` + +Encode this example as a test. + +## 7. Reload the SQL seeds before each test run + +Running the SQL code present in the seed file will empty the table and re-insert the seed data. + +This is so you get a fresh table contents every time you run the test suite. + +```ruby +# EXAMPLE + +# file: spec/student_repository_spec.rb + +def reset_students_table + seed_sql = File.read('spec/seeds_students.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'students' }) + connection.exec(seed_sql) +end + +describe StudentRepository do + before(:each) do + reset_students_table + end + + # (your tests will go here). +end +``` + +## 8. Test-drive and implement the Repository class behaviour + +_After each test you write, follow the test-driving process of red, green, refactor to implement the behaviour._ diff --git a/lib/app_test.rb b/lib/app_test.rb new file mode 100644 index 00000000..6dd2cc9b --- /dev/null +++ b/lib/app_test.rb @@ -0,0 +1,54 @@ +require_relative 'database_connection' +require_relative 'item_repository' +require_relative 'order_repository' + +class Application + + def initialize(database_name, io = Kernel, item_repository = ItemRepository.new, order_repository = OrderRepository.new) + DatabaseConnection.connect(database_name) + @io = io + @item_repository = item_repository + @order_repository = order_repository + end + + def run + @io.puts 'Welcome to the shop management program!' + list_choices + choice = @io.gets.chomp + case choice + when '1' + @item_repository.all.each_with_index do |item, i| + @io.puts "* #{i+1} - #{item.name}" + end + when '2' + # create a new item + when '3' + # list all orders + when '4' + # create a new order + end + + end + + private + + def list_choices + @io.puts "\nWhat do you like to do?" + @io.puts '1 - List all shop items' + @io.puts '2 - Create a new item' + @io.puts '3 - List all orders' + @io.puts '4 - Create a new order' + @io.print 'Enter your choice: ' + end + +end + +if __FILE__ == $0 + app = Application.new( + 'items_orders_test', + Kernel, + ItemRepository.new, + 'OrderRepository.new' + ) + app.run +end \ No newline at end of file diff --git a/lib/database_connection.rb b/lib/database_connection.rb new file mode 100644 index 00000000..c8f1ad1d --- /dev/null +++ b/lib/database_connection.rb @@ -0,0 +1,28 @@ +# file: lib/database_connection.rb + +require 'pg' + +# This class is a thin "wrapper" around the +# PG library. We'll use it in our project to interact +# with the database using SQL. + +class DatabaseConnection + # This method connects to PostgreSQL using the + # PG gem. We connect to 127.0.0.1, and select + # the database name given in argument. + def self.connect(database_name) + @connection = PG.connect({ host: '127.0.0.1', dbname: database_name }) + end + + # This method executes an SQL query + # on the database, providing some optional parameters + # (you will learn a bit later about when to provide these parameters). + def self.exec_params(query, params) + if @connection.nil? + raise 'DatabaseConnection.exec_params: Cannot run a SQL query as the connection to'\ + 'the database was never opened. Did you make sure to call first the method '\ + '`DatabaseConnection.connect` in your app.rb file (or in your tests spec_helper.rb)?' + end + @connection.exec_params(query, params) + end +end \ No newline at end of file diff --git a/lib/item.rb b/lib/item.rb new file mode 100644 index 00000000..a3a1a87d --- /dev/null +++ b/lib/item.rb @@ -0,0 +1,4 @@ +class Item + attr_accessor :id, :name, :unit_price, :quantity, :orders +end + \ No newline at end of file diff --git a/lib/item_repository.rb b/lib/item_repository.rb new file mode 100644 index 00000000..4769538f --- /dev/null +++ b/lib/item_repository.rb @@ -0,0 +1,38 @@ +require_relative './item' + +class ItemRepository + + def all + sql = 'SELECT id, name, unit_price, quantity FROM items;' + result_set = DatabaseConnection.exec_params(sql, []) + + items =[] + result_set.each do|record| + # item = Item.new + # item.id = record['id'] + # item.name = record['name'] + # item.unit_price= record['unit_price'] + # item.quantity= record['quantity'] + items << record_to_item_object(record) + end + return items + + end + + def record_to_item_object(record) + item = Item.new + item.id = record['id'] + item.name = record['name'] + item.unit_price= record['unit_price'] + item.quantity= record['quantity'] + return item + end + + def create(item) + sql = 'INSERT INTO items (name, unit_price, quantity) VALUES($1, $2, $3);' + sql_params = [item.name, item.unit_price, item.quantity] + DatabaseConnection.exec_params(sql, sql_params) + return nil + end + +end \ No newline at end of file diff --git a/lib/order.rb b/lib/order.rb new file mode 100644 index 00000000..dcffd863 --- /dev/null +++ b/lib/order.rb @@ -0,0 +1,7 @@ +class Order + attr_accessor :id, :customer_name, :placed_date, :items + + def initialize + @items = [] + end +end \ No newline at end of file diff --git a/lib/order_repository.rb b/lib/order_repository.rb new file mode 100644 index 00000000..a1cef769 --- /dev/null +++ b/lib/order_repository.rb @@ -0,0 +1,56 @@ +require_relative './order' + +class OrderRepository + + def all + + sql = 'SELECT id, customer_name, placed_date FROM orders;' + result_set = DatabaseConnection.exec_params(sql, []) + + orders =[] + result_set.each do |record| + orders << record_to_order_object(record) + end + + return orders + end + + def record_to_order_object(record) + order = Order.new + order.id = record['id'] + order.customer_name = record['customer_name'] + order.placed_date = record['placed_date'] + return order + end + + def create(order) + sql = 'INSERT INTO orders (customer_name, placed_date) VALUES($1, $2);' + + sql_params = [order.customer_name, order.placed_date] + + DatabaseConnection.exec_params(sql, sql_params) + return nil + end + + def find_with_items(id) + sql = 'SELECT items.id, items.name + FROM items + JOIN items_orders ON items_orders.item_id = items.id + JOIN orders ON items_orders.order_id = orders.id + WHERE orders.id = $1;' + params = [id] + + result = DatabaseConnection.exec_params(sql, params) + order = Order.new + order.id = result.first['id'] + # order.name = result.first['name'] + + result.each do |record| + item = Item.new + item.id = record['item_id'] + order.items << item + end + return order + end + +end \ No newline at end of file diff --git a/orders_class_design.md b/orders_class_design.md new file mode 100644 index 00000000..b4f59e82 --- /dev/null +++ b/orders_class_design.md @@ -0,0 +1,200 @@ +# Orders Model and Repository Classes Design Recipe + +_Copy this recipe template to design and implement Model and Repository classes for a database table._ + +## 1. Design and create the Table + +If the table is already created in the database, you can skip this step. + +Otherwise, [follow this recipe to design and create the SQL schema for your table](./single_table_design_recipe_template.md). + +_In this template, we'll use an example table `students`_ + +``` +# EXAMPLE + +Table: orders + +Columns: +id | customer_name | placed_date +``` + +## 2. Create Test SQL seeds + +Your tests will depend on data stored in PostgreSQL to run. + +If seed data is provided (or you already created it), you can skip this step. + +````sql +-- EXAMPLE +-- (file: spec/seeds_{table_name}.sql) + +-- Write your SQL seed here. + +-- First, you'd need to truncate the table - this is so our table is emptied between each test run, +-- so we can start with a fresh state. +-- (RESTART IDENTITY resets the primary key) + +TRUNCATE TABLE items, items_orders, orders RESTART IDENTITY; -- replace with your own table name. + +-- Below this line there should only be `INSERT` statements. +-- Replace these statements with your own seed data. + +INSERT INTO orders (customer_name, placed_date) VALUES ('David', '2023/04/22'); +INSERT INTO orders (customer_name, placed_date) VALUES ('James', '2023/05/25'); + +Run this SQL file on the database to truncate (empty) the table, and insert the seed data. Be mindful of the fact any existing records in the table will be deleted. + +```bash +psql -h 127.0.0.1 items_orders_test < spec/seeds_orders.sql +```` + +## 3. Define the class names + +Usually, the Model class name will be the capitalised table name (single instead of plural). The same name is then suffixed by `Repository` for the Repository class name. + +```ruby +# EXAMPLE +# Table name: students + +# Model class +# (in lib/item.rb) +class Order +end + +# Repository class +# (in lib/item_repository.rb) +class OrdersRepository +end +``` + +## 4. Implement the Model class + +Define the attributes of your Model class. You can usually map the table columns to the attributes of the class, including primary and foreign keys. + +````ruby +# EXAMPLE +# Table name: students + +# Model class +# (in lib/item.rb) + +class Order + attr_accessor :id, :customer_name, :placed_date +end + + + + +_You may choose to test-drive this class, but unless it contains any more logic than the example above, it is probably not needed._ + +## 5. Define the Repository Class interface + +Your Repository class will need to implement methods for each "read" or "write" operation you'd like to run against the database. + +Using comments, define the method signatures (arguments and return value) and what they do - write up the SQL queries that will be used by each method. + +```ruby +# EXAMPLE +# Table name: items + +# Repository class +# (in lib/item_repository.rb) + +class OrderRepository + + # Selecting all orders + # No arguments + def all + # Executes the SQL query: + # SELECT id, customer_name, placed_date FROM orders; + + # Returns an array of Order objects. + end + + # Gets a single record by its ID + # One argument: the id (number) + def find(id) + # Executes the SQL query: + # SELECT id, name, cohort_name FROM students WHERE id = $1; + + # Returns a single Student object. + end + + # Add more methods below for each operation you'd like to implement. + + # def create(student) + # end + + # def update(student) + # end + + # def delete(student) + # end +end +```` + +## 6. Write Test Examples + +Write Ruby code that defines the expected behaviour of the Repository class, following your design from the table written in step 5. + +These examples will later be encoded as RSpec tests. + +```ruby +# EXAMPLES + +# 1 +# Get all students + +repo = ItemRepository.new + +items = repo.all + +items.length # => 2 +items.first.name # => "book" + +# 2 +# Get a single student + +repo = StudentRepository.new + +student = repo.find(1) + +student.id # => 1 +student.name # => 'David' +student.cohort_name # => 'April 2022' + +# Add more examples for each method +``` + +Encode this example as a test. + +## 7. Reload the SQL seeds before each test run + +Running the SQL code present in the seed file will empty the table and re-insert the seed data. + +This is so you get a fresh table contents every time you run the test suite. + +```ruby +# EXAMPLE + +# file: spec/student_repository_spec.rb + +def reset_students_table + seed_sql = File.read('spec/seeds_students.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'students' }) + connection.exec(seed_sql) +end + +describe StudentRepository do + before(:each) do + reset_students_table + end + + # (your tests will go here). +end +``` + +## 8. Test-drive and implement the Repository class behaviour + +_After each test you write, follow the test-driving process of red, green, refactor to implement the behaviour._ diff --git a/seeds.sql b/seeds.sql new file mode 100644 index 00000000..517871d3 --- /dev/null +++ b/seeds.sql @@ -0,0 +1,23 @@ +-- Create the first table. +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + name text, + unit_price int, + quantity int +); + +-- Create the second table. +CREATE TABLE orders ( + id SERIAL PRIMARY KEY, + customer_name text, + placed_date date +); + +-- Create the join table. +CREATE TABLE items_orders ( + item_id int, + order_id int, + constraint fk_post foreign key(item_id) references items(id) on delete cascade, + constraint fk_tag foreign key(order_id) references orders(id) on delete cascade, + PRIMARY KEY (item_id, order_id) +); \ No newline at end of file diff --git a/spec/app_test_spec.rb b/spec/app_test_spec.rb new file mode 100644 index 00000000..2e4defbd --- /dev/null +++ b/spec/app_test_spec.rb @@ -0,0 +1,84 @@ +require 'app_test' +def reset_items_table + seed_sql = File.read('spec/seeds_items.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'items_orders_test' }) + connection.exec(seed_sql) +end + + +RSpec.describe Application do + before(:each) do + reset_items_table + end + + it "constructs" do + io_dbl = double :Kernel + item_repository_dbl = double :item_repository + order_repository_dbl = double :order_repository + app = Application.new('items_orders_test', io_dbl, item_repository_dbl, order_repository_dbl) + end + + describe 'Terminal behaviour' do + it 'welcomes the user and presents four options with a prompt' do + io_dbl = double :Kernel + item_repository_dbl = double :item_repository + order_repository_dbl = double :order_repository + app = Application.new('items_orders_test', io_dbl, item_repository_dbl, order_repository_dbl) + + expect(io_dbl).to receive(:puts) + .with("Welcome to the shop management program!").ordered + expect(io_dbl).to receive(:puts) + .with("\nWhat do you like to do?").ordered + expect(io_dbl).to receive(:puts) + .with("1 - List all shop items").ordered + expect(io_dbl).to receive(:puts) + .with("2 - Create a new item").ordered + expect(io_dbl).to receive(:puts) + .with("3 - List all orders").ordered + expect(io_dbl).to receive(:puts) + .with("4 - Create a new order").ordered + expect(io_dbl).to receive(:print) + .with("Enter your choice: ").ordered + expect(io_dbl).to receive(:gets) + .and_return('choice').ordered + app.run + end + + context "when the user chooses 1 - List all shop items" do + it 'lists all items' do + io_dbl = double :Kernel + item_repository_dbl = double :item_repository + item1 = double :item, name: 'book' + item2 = double :item, name: 'pen' + order_repository_dbl = double :order_repository + app = Application.new('items_orders_test', io_dbl, item_repository_dbl, order_repository_dbl) + + + expect(io_dbl).to receive(:puts) + .with("Welcome to the shop management program!").ordered + expect(io_dbl).to receive(:puts) + .with("\nWhat do you like to do?").ordered + expect(io_dbl).to receive(:puts) + .with("1 - List all shop items").ordered + expect(io_dbl).to receive(:puts) + .with("2 - Create a new item").ordered + expect(io_dbl).to receive(:puts) + .with("3 - List all orders").ordered + expect(io_dbl).to receive(:puts) + .with("4 - Create a new order").ordered + expect(io_dbl).to receive(:print) + .with("Enter your choice: ").ordered + expect(io_dbl).to receive(:gets) + .and_return('1').ordered + expect(item_repository_dbl).to receive(:all) + .and_return([item1, item2]).ordered + expect(io_dbl).to receive(:puts) + .with("* 1 - book").ordered + expect(io_dbl).to receive(:puts) + .with("* 2 - pen").ordered + + app.run + end + end + end +end \ No newline at end of file diff --git a/spec/item_repository_spec.rb b/spec/item_repository_spec.rb new file mode 100644 index 00000000..21bad42a --- /dev/null +++ b/spec/item_repository_spec.rb @@ -0,0 +1,41 @@ +require "item_repository" + +def reset_items_table + seed_sql = File.read('spec/seeds_items.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'items_orders_test' }) + connection.exec(seed_sql) +end + +RSpec.describe ItemRepository do + + before(:each) do + reset_items_table + end + + it "return all items" do + repo = ItemRepository.new + items = repo.all + expect(items.length).to eq(2) + expect(items.first.name).to eq("book") + end + + it "create a new item" do + repo = ItemRepository.new + item = Item.new + item.name = 'monitor' + item.unit_price = '100' + item.quantity = '50' + expect(repo.create(item)).to eq(nil) + repo.create(item) + all_items = repo.all + expect(all_items).to include( + have_attributes( + name: item.name, + unit_price: "100", + quantity: "50" + ) + ) +end + +end + diff --git a/spec/order_repository_spec.rb b/spec/order_repository_spec.rb new file mode 100644 index 00000000..b9d6a512 --- /dev/null +++ b/spec/order_repository_spec.rb @@ -0,0 +1,46 @@ +require "order_repository" + +def reset_orders_table + seed_sql = File.read('spec/seeds_orders.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'items_orders_test' }) + connection.exec(seed_sql) +end + +RSpec.describe OrderRepository do + + before(:each) do + reset_orders_table + end + + it "return all orders" do + repo = OrderRepository.new + orders = repo.all + expect(orders.length).to eq(2) + expect(orders.first.customer_name).to eq("David") + end + + it "create a new order" do + repo = OrderRepository.new + order = Order.new + order.customer_name = 'Taylor' + order.placed_date = '2023-04-01' + expect(repo.create(order)).to eq(nil) + repo.create(order) + all_orders = repo.all + expect(all_orders).to include( + have_attributes( + customer_name: order.customer_name, + placed_date: '2023-04-01' + ) + ) + end + + it 'finds order 1 with corresponding items' do + repo = OrderRepository.new + order = repo.find_with_items(1) + + expect(order.items.length).to eq(2) + expect(order.id).to eq('1') + end +end + diff --git a/spec/seeds_items.sql b/spec/seeds_items.sql new file mode 100644 index 00000000..2c88df35 --- /dev/null +++ b/spec/seeds_items.sql @@ -0,0 +1,17 @@ +-- (file: spec/seeds_{table_name}.sql) + +-- Write your SQL seed here. + +-- First, you'd need to truncate the table - this is so our table is emptied between each test run, +-- so we can start with a fresh state. +-- (RESTART IDENTITY resets the primary key) + +TRUNCATE TABLE items, items_orders, orders RESTART IDENTITY; + -- replace with your own table name. + +-- Below this line there should only be `INSERT` statements. +-- Replace these statements with your own seed data. + +INSERT INTO items (name, unit_price, quantity) VALUES ('book', '2', '100'); +INSERT INTO items (name, unit_price, quantity) VALUES ('pen', '1', '200'); + diff --git a/spec/seeds_orders.sql b/spec/seeds_orders.sql new file mode 100644 index 00000000..6c5103fc --- /dev/null +++ b/spec/seeds_orders.sql @@ -0,0 +1,24 @@ +-- (file: spec/seeds_{table_name}.sql) + +-- Write your SQL seed here. + +-- First, you'd need to truncate the table - this is so our table is emptied between each test run, +-- so we can start with a fresh state. +-- (RESTART IDENTITY resets the primary key) + +TRUNCATE TABLE items, items_orders, orders RESTART IDENTITY; -- replace with your own table name. + +-- Below this line there should only be `INSERT` statements. +-- Replace these statements with your own seed data. + + + +INSERT INTO items (name, unit_price, quantity) VALUES ('book', '2', '100'); +INSERT INTO items (name, unit_price, quantity) VALUES ('pen', '1', '200'); + +INSERT INTO orders (customer_name, placed_date) VALUES ('David', '2023/04/22'); +INSERT INTO orders (customer_name, placed_date) VALUES ('James', '2023/05/25'); + +INSERT INTO items_orders (item_id, order_id) VALUES (1,1); +INSERT INTO items_orders (item_id, order_id) VALUES (2,1); +INSERT INTO items_orders (item_id, order_id) VALUES (1,2); \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 252747d8..b5fe2f0b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,10 +1,19 @@ +# file: spec/spec_helper.rb + +require 'database_connection' + +# Make sure this connects to your test database +# (its name should end with '_test') +DatabaseConnection.connect('items_orders_test') + + require 'simplecov' require 'simplecov-console' SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::Console, # Want a nice code coverage website? Uncomment this next line! - # SimpleCov::Formatter::HTMLFormatter + SimpleCov::Formatter::HTMLFormatter ]) SimpleCov.start diff --git a/two_tables_design.md b/two_tables_design.md new file mode 100644 index 00000000..decf3800 --- /dev/null +++ b/two_tables_design.md @@ -0,0 +1,157 @@ +# Two Tables (Many-to-Many) Design Recipe Template + +_Copy this recipe template to design and create two related database tables from a specification._ + +## 1. Extract nouns from the user stories or specification + +``` +As a shop manager +So I can know which items I have in stock +I want to keep a list of my shop items with their name and unit price. + +As a shop manager +So I can know which items I have in stock +I want to know which quantity (a number) I have for each item. + +As a shop manager +So I can manage items +I want to be able to create a new item. + +As a shop manager +So I can know which orders were made +I want to keep a list of orders with their customer name. + +As a shop manager +So I can know which orders were made +I want to assign each order to their corresponding item. + +As a shop manager +So I can know which orders were made +I want to know on which date an order was placed. + +As a shop manager +So I can manage orders +I want to be able to create a new order. + +``` + +Nouns: + +items: name , unit price , quantity +orders: customer names, placed_date + +``` + +## 2. Infer the Table Name and Columns + +Put the different nouns in this table. Replace the example with your own nouns. + +| Record | Properties | +| --------------------- | ------------------ | +| items | name , unit_price , quantity +| orders | customer_name, placed_date + +1. Name of the first table (always plural): `items ` + + Column names: name , unit_price , quantity + +2. Name of the second table (always plural): orders + + Column names: customer_name, placed_date + +## 3. Decide the column types. + +[Here's a full documentation of PostgreSQL data types](https://www.postgresql.org/docs/current/datatype.html). + +Most of the time, you'll need either `text`, `int`, `bigint`, `numeric`, or `boolean`. If you're in doubt, do some research or ask your peers. + +Remember to **always** have the primary key `id` as a first column. Its type will always be `SERIAL`. + + +# EXAMPLE: + +Table: items +id: SERIAL +name text +unit_price int +quantity int + +Table: orders +id: SERIAL +customer_name text +placed_date date + +``` + +## 4. Design the Many-to-Many relationship + +1. Can one item have many orders? YES +2. Can one order have many items? Yes + +## 5. Design the Join Table + +The join table usually contains two columns, which are two foreign keys, each one linking to a record in the two other tables. + +The naming convention is `table1_table2`. + +``` + +# EXAMPLE + +Table: items +id: SERIAL +name text +unit_price int +quantity int + +Table: orders +id: SERIAL +customer_name text +placed_date date + +Join table for tables: items and orders +Join table name: items_orders +Columns: item_id, order_id + +``` + +## 4. Write the SQL. + +```sql +-- EXAMPLE +-- file: posts_tags.sql + +-- Replace the table name, columm names and types. + +-- Create the first table. +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + name text, + unit_price int, + quantity int +); + +-- Create the second table. +CREATE TABLE orders ( + id SERIAL PRIMARY KEY, + customer_name text, + placed_date date +); + +-- Create the join table. +CREATE TABLE items_orders ( + item_id int, + order_id int, + constraint fk_post foreign key(item_id) references items(id) on delete cascade, + constraint fk_tag foreign key(order_id) references orders(id) on delete cascade, + PRIMARY KEY (item_id, order_id) +); + +``` + +## 5. Create the tables. + +```bash +psql -h 127.0.0.1 items_orders < items_orders.sql + +```