feat: implement database-backed student management with MySQL and add persistence strategies

lab4
Artem-Darius Weber 2 months ago
parent 7896de3c3e
commit da53a9c4ae

@ -5,6 +5,74 @@
```mermaid
classDiagram
class PersistenceStrategy {
+ load(filename : String) : Raise
+ save(filename : String, students : Students) : Raise
}
class StudentsListBase {
+ initialize(filename : String)
+ load_from_file() : Raise
+ save_to_file() : Raise
+ get_student_by_id(id : String) : Student
+ get_k_n_student_short_list(k : Int, n : Int, data_list = nil) : List<Student>
+ sort_students() : List<Student>
+ add_student(student : Student) : Student
+ update_student_by_id(id : Int, new_stodent : Student) : Bool
+ delete_student_by_id(id : Int)
+ get_student_short_count() : Int
}
class StudentsListDB {
+ initialize()
+ get_student_by_id(id : Int) : nil | Student
+ get_k_n_student_short_list(k : Int, n : Int) : List<Student>
+ add_student(student : Student) : Student
+ update_student_by_id(id : Int, new_student : Student)
+ delete_student_by_id(id : Int)
+ get_student_short_count() : Int
- row_to_student(row : Student)
- escape(value : any)
}
class JSONPersistenceStrategy {
+ load(filename : String) : List<>
+ save(filename : String, students : List<Student>)
- student_to_hash(student : Student)
- hash_to_student(hash : String)
}
class TXTPersistenceStrategy {
+ load(filename : String) : List<>
+ save(filename : String, students : List<Student>)
}
class YAMLPersistenceStrategy {
+ load(filename : String) : List<>
+ save(filename : String, students : List<Student>)
- student_to_hash(student : Student)
- hash_to_student(hash : String)
}
class StudentsList {
+ initialize(filename : String, persistence_strategy)
+ load() : self
+ save() : self
+ get_student_by_id(id : Int) : Student
+ get_k_n_student_short_list(k : Int, n : Int, data_list = nil) : DataListStudentShort
+ sort_students() : List<Student>
add_student(student : Student) : Student
+ update_student_by_id(id : Int, new_student : Student) : Bool
+ delete_student_by_id(id : Int) : Bool
+ get_student_short_count() : Int
}
class DatabaseConnection {
+ initialize()
+ client()
+ query(sql : String)
}
class BinarySearchTree {
- root : Node
+ add(student : Student) : void
@ -81,6 +149,12 @@ classDiagram
BinarySearchTree o-- Node
Node o-- Student
StudentsListBase o-- Student
StudentsListDB o-- DatabaseConnection
DatabaseConnection <.. Singleton
JSONPersistenceStrategy o-- PersistenceStrategy
TXTPersistenceStrategy o-- PersistenceStrategy
YAMLPersistenceStrategy o-- PersistenceStrategy
Person <|-- Student
Person <|-- StudentShort
Student <.. StudentRepository

@ -0,0 +1,20 @@
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: mysql_container
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: project_db
MYSQL_USER: project_user
MYSQL_PASSWORD: project_password
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./db/migrations:/docker-entrypoint-initdb.d # Автоматическая инициализация базы (скрипты миграций)
volumes:
mysql_data:

@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS student (
id INT AUTO_INCREMENT PRIMARY KEY,
git VARCHAR(255) NOT NULL,
surname VARCHAR(100) NOT NULL,
name VARCHAR(100) NOT NULL,
patronymic VARCHAR(100) NOT NULL,
birth_date DATE NOT NULL,
phone VARCHAR(20),
telegram VARCHAR(50),
email VARCHAR(100)
);

@ -0,0 +1,4 @@
INSERT INTO student (git, surname, name, patronymic, birth_date, phone, telegram, email)
VALUES
('https://github.com/example', 'Иванов', 'Иван', 'Иванович', '2000-01-01', '+123456789', '@telegram', 'email@example.com'),
('https://github.com/example2', 'Петров', 'Петр', 'Петрович', '1999-05-15', '+987654321', '@petrov', 'petrov@example.com');

@ -0,0 +1,23 @@
require 'mysql2'
require 'singleton'
class DatabaseConnection
include Singleton
def initialize
@client = Mysql2::Client.new(
host: 'localhost',
username: 'project_user',
password: 'project_password',
database: 'project_db'
)
end
def client
@client
end
def query(sql)
@client.query(sql)
end
end

@ -0,0 +1,101 @@
require_relative '../db_connection'
require_relative '../student'
require_relative '../student_short'
require_relative '../data_list_student_short'
require 'date'
class StudentsListDB
def initialize
@db = DatabaseConnection.instance
end
# Получить объект Student по ID.
def get_student_by_id(id)
result = @db.query("SELECT * FROM student WHERE id = #{id} LIMIT 1")
row = result.first
row ? row_to_student(row) : nil
end
# Получить список из k студентов (страница n) в виде объекта DataList.
def get_k_n_student_short_list(k, n)
offset = (n - 1) * k
results = @db.query("SELECT * FROM student ORDER BY surname, name, patronymic LIMIT #{k} OFFSET #{offset}")
student_shorts = results.map { |row| StudentShort.from_student(row_to_student(row)) }
DataListStudentShort.new(student_shorts)
end
# Добавить объект Student в БД (ID формируется автоматически).
def add_student(student)
sql = <<~SQL
INSERT INTO student (git, surname, name, patronymic, birth_date, phone, telegram, email)
VALUES (
'#{escape(student.git)}',
'#{escape(student.surname)}',
'#{escape(student.name)}',
'#{escape(student.patronymic)}',
'#{student.birth_date}',
#{student.phone ? "'#{escape(student.phone)}'" : "NULL"},
#{student.telegram ? "'#{escape(student.telegram)}'" : "NULL"},
#{student.email ? "'#{escape(student.email)}'" : "NULL"}
)
SQL
@db.query(sql)
new_id = @db.query("SELECT LAST_INSERT_ID() as id").first['id']
student.id = new_id.to_s
student
end
# Обновить студента по ID.
def update_student_by_id(id, new_student)
sql = <<~SQL
UPDATE student SET
git = '#{escape(new_student.git)}',
surname = '#{escape(new_student.surname)}',
name = '#{escape(new_student.name)}',
patronymic = '#{escape(new_student.patronymic)}',
birth_date = '#{new_student.birth_date}',
phone = #{new_student.phone ? "'#{escape(new_student.phone)}'" : "NULL"},
telegram = #{new_student.telegram ? "'#{escape(new_student.telegram)}'" : "NULL"},
email = #{new_student.email ? "'#{escape(new_student.email)}'" : "NULL"}
WHERE id = #{id}
SQL
@db.query(sql)
@db.client.affected_rows > 0
end
# Удалить студента по ID.
def delete_student_by_id(id)
@db.query("DELETE FROM student WHERE id = #{id}")
@db.client.affected_rows > 0
end
# Получить количество студентов.
def get_student_short_count
result = @db.query("SELECT COUNT(*) as count FROM student")
result.first['count']
end
private
# Преобразование строки результата в объект Student.
def row_to_student(row)
Student.new(
id: row['id'].to_s,
git: row['git'],
surname: row['surname'],
name: row['name'],
patronymic: row['patronymic'],
birth_date: Date.parse(row['birth_date'].to_s),
phone: row['phone'],
telegram: row['telegram'],
email: row['email']
)
end
# Простой метод для экранирования строк (на практике лучше использовать подготовленные выражения).
def escape(value)
@db.client.escape(value.to_s)
end
end

@ -17,6 +17,20 @@ class Student < Person
self.birth_date = birth_date
end
def self.new_from_hash(hash)
new(
id: hash[:id] || hash['id'],
git: hash[:git] || hash['git'],
surname: hash[:surname] || hash['surname'],
name: hash[:name] || hash['name'],
patronymic: hash[:patronymic] || hash['patronymic'],
birth_date: hash[:birth_date].is_a?(Date) ? hash[:birth_date] : Date.parse(hash[:birth_date].to_s),
phone: hash[:phone] || hash['phone'],
telegram: hash[:telegram] || hash['telegram'],
email: hash[:email] || hash['email']
)
end
def self.from_string(student_string)
parts = student_string.split('|').map(&:strip)
raise ArgumentError, 'Invalid student string format' if parts.size < 7

@ -0,0 +1,18 @@
require_relative '../db_connection'
require_relative '../providers/students_list_db'
students_db = StudentsListDB.new
# Пример: получение студента с ID = 1
student = students_db.get_student_by_id(1)
puts student ? student.get_info : "Студент не найден"
# Пример: получение второй страницы по 2 записи (если записей достаточно)
data_list = students_db.get_k_n_student_short_list(2, 2)
puts "Количество студентов: #{students_db.get_student_short_count}"
data_table = data_list.get_data
(0...data_table.rows_count).each do |row|
row_data = (0...data_table.columns_count).map { |col| data_table.item(row, col) }
puts row_data.join(" | ")
end
Loading…
Cancel
Save