Справка
Телеграм чат начинающих программистов. Общаемся и помогаем друг другу
Если ссылка не открывается, можно найти нас в поиске по чатам @rubyrush
или
пойти другим путем
На прошлом уроке мы познакомились с понятием баз данных. В этом уроке мы подключим базу данных SQlite к нашей программе на ruby: блокноту.
СУБД может состоять из двух приложений: одно (сервер) постоянно запущено в отдельном процессе и крутится в памяти процессора, а другое (клиент) соединяется с первым для отправки SQL-запросов. Такие СУБД называются клиент-серверными, к ним, например, относятся СУБД MySQL и PostgreSQL.
SQlite не является клиент-серверной СУБД. В случае с SQLite приложение для работы с данными запускаться по мере необходимости. В любом случае СУБД всегда предоставляет какой-то интерфейс для обращений к ней. Чтобы пользоваться обращениями в (практически) любом языке программирования к БД обычно есть подходящие библиотеки, где этот интерфейс реализован.
Мы хотим, чтобы наш дневник пользовался SQlite-базой для хранения всех записей. Программа должна сохранять новую запись в базе при добавлении и по запросу пользователя выводить последние записи определенного типа (Memo
, Link
, Task
).
Каждая таблица в БД представляет какую-то сущность, например, содержит записи для каждого из объектов какого-то класса.
Для удобства все записи мы будем хранить в одной таблице posts
. Как и у класса, поля в этой таблице играют ту же роль, это просто свойства этой сущности. Разумно назвать поля таблицы также, как и поля класса.
Но мы не хотим делать отдельную таблицу для каждого из дочерних классов: Memo
, Link
и Task
. Тогда у этой таблицы должен быть исчерпывающий набор полей: чтобы каким бы ни была запись, мы бы могли её уложить в строку этой таблицы. Также у нас в таблице должно быть поле type
, чтобы мы могли понять, что это за запись.
С помощью нашего SQlite менеджера создадим базу данных notepad.sqlite
в папке нашего урока c:\rubytut2\lesson12
и в ней создайте таблицу для нашего блокнота. Внимательно следите за типами полей.
Для того, чтобы добавить в наши программы методы работы с базами данных SQLite мы должны установить соответствующую библиотеку. Она называется sqlite3.
Откройте папку c:\dev
в проводнике и запустите нашу улучшенную консоль разработчика msys.bat
. В ней наберите команду для установки:
gem install sqlite3 --platform=ruby -- --with-sqlite3-include=/c:/sqlite3/ --with-sqlite3-lib=/c:/sqlite3/.libs/
Если всё прошло гладко, то нужно выполнить ещё одно действие: скопируйте библиотеку c:\sqlite\.libs\libsqlite3-0.dll
в папку c:\Ruby22\bin
(или в ту папку, куда вы устанавливали Rubу).
Сейчас мы напишем у класса Post
метод save_to_db
, который будет брать данные заметки и создавать в базе строку с этими данными. Для этого мы воспользуемся SQL-запросом INSERT такого вида:
INSERT INTO posts (type, body, created_at) VALUES ('Memo', 'Мне приснился странный сок', '2015.07.08, 10:00')
Также, напишем метод find
, который будет либо искать конкретную запись (если ему передали ID этой записи), либо выводить последние 10 записей в нашем блокноте.
SELECT * FROM posts LIMIT 10;
Приступаем непосредственно к написанию кода на ruby. Внимательно читайте комментарии к коду, лишние комментарии (комментарии к предыдущим версиям были удалены для удобства):
post.rb
:
# Во-первых, сразу подключим sqlite3
# Подключать библиотеки лучше всего в тех файлах/классах, который их используют
require 'sqlite3'
class Post
# Статическое поле класса или class variable
# аналогично статическим методам принадлежит всему классу в целом
# и доступно незвисимо от созданных объектов
@@SQLITE_DB_FILE = 'notepad.sqlite'
# Теперь нам нужно будет читать объекты из базы данных
# поэтому удобнее всегда иметь под рукой связь между классом и его именем в виде строки
def self.post_types
{'Memo' => Memo, 'Task' => Task, 'Link' => Link}
end
# Параметром теперь является строковое имя нужного класса
def self.create(type)
return post_types[type].new
end
def initialize
@created_at = Time.now # дата создания записи
@text = nil # массив строк записи — пока пустой
end
def read_from_console
end
def to_strings
end
# Метод to_db_hash возвращает хэш вида {'имя_столбца' => 'значение'}
# для сохранения в базу данных новой записи
def to_db_hash
# дочерние классы сами знают свое представление, но общие для всех классов поля
# можно заполнить уже сейчас в базовом классе!
{
# self — ключевое слово, указывает на 'этот объект',
# то есть конкретный экземпляр класса, где выполняется в данный момент этот код
'type' => self.class.name,
'created_at' => @created_at.to_s
}
# todo: дочерние классы должны дополнять этот хэш массив своими полями
end
# Наконец, вот он метод, сохраняющий состояние объекта в базу данных
def save_to_db
db = SQLite3::Database.open(@@SQLITE_DB_FILE) # открываем "соединение" к базе SQLite
db.results_as_hash = true # настройка соединения к базе, он результаты из базы преобразует в Руби хэши
# запрос к базе на вставку новой записи в соответствии с хэшом, сформированным дочерним классом to_db_hash
db.execute(
"INSERT INTO posts (" +
to_db_hash.keys.join(', ') + # все поля, перечисленные через запятую
") " +
" VALUES ( " +
('?,'*to_db_hash.keys.size).chomp(',') + # строка из заданного числа _плейсхолдеров_ ?,?,?...
")",
to_db_hash.values # массив значений хэша, которые будут вставлены в запрос вместо _плейсхолдеров_
)
insert_row_id = db.last_insert_row_id
# закрываем соединение
db.close
# возвращаем идентификатор записи в базе
return insert_row_id
end
# Получает на вход хэш массив данных и должен заполнить свои поля
def load_data(data_hash)
@created_at = Time.parse(data_hash['created_at'])
# todo: остальные специфичные поля должны заполнить дочерние классы
end
def save
file = File.new(file_path, "w:UTF-8")
for item in to_strings do
file.puts(item)
end
file.close
end
def file_path
current_path = File.dirname(__FILE__)
file_name = @created_at.strftime("#{self.class.name}_%Y-%m-%d_%H-%M-%S.txt")
return current_path + "/" + file_name
end
# Находит в базе запись по идентификатору или массив записей
# из базы данных, который можно например показать в виде таблицы на экране
def self.find(limit, type, id)
db = SQLite3::Database.open(@@SQLITE_DB_FILE) # открываем "соединение" к базе SQLite
if id.present?
db.results_as_hash = true # настройка соединения к базе, он результаты из базы преобразует в Руби хэши
# выполняем наш запрос, он возвращает массив результатов, в нашем случае из одного элемента
result = db.execute("SELECT * FROM posts WHERE rowid = ?", id)
# получаем единственный результат (если вернулся массив)
result = result[0] if result.is_a? Array
db.close
if result.empty?
puts "Такой id #{id} не найден в базе :("
return nil
else
# создаем с помощью нашего же метода create экземпляр поста,
# тип поста мы взяли из массива результатов [:type]
# номер этого типа в нашем массиве post_type нашли с помощью метода Array#find_index
post = create(result['type'])
# заполним этот пост содержимым
post.load_data(result)
# и вернем его
return post
end
# эта ветвь выполняется если не передан идентификатор
else
db.results_as_hash = false # настройка соединения к базе, он результаты из базы НЕ преобразует в Руби хэши
# формируем запрос в базу с нужными условиями
query = "SELECT rowid, * FROM posts "
query += "WHERE type = :type " unless type.nil? # если задан тип, надо добавить условие
query += "ORDER by rowid DESC " # и наконец сортировка - самые свежие в начале
query += "LIMIT :limit " unless limit.nil? # если задан лимит, надо добавить условие
# готовим запрос в базу, как плов :)
statement = db.prepare query
statement.bind_param('type', type) unless type.nil? # загружаем в запрос тип вместо плейсхолдера, добавляем лук :)
statement.bind_param('limit', limit) unless limit.nil? # загружаем лимит вместо плейсхолдера, добавляем морковь :)
result = statement.execute! #(query) # выполняем
statement.close
db.close
return result
end
end
end
# PS: метод self.find получился довольно громоздким и со множеством if — это не хороший стиль.
#
# Подумайте и попробуйте его сделать изящнее и проще.
# Например разбив его на несколько других методов, или переработав его логику (например так,
# чтобы он работал универсальным образом — всегда возвращал массив
# объектов Post. просто в случае с id этот массив будет состоять из одного объекта)
#
# Кстати, подобным "правильным" образом работает похожий метод в Ruby on Rails
Теперь давайте реализуем у каждого ребёнка методы to_db_hash
и load_data
:
link.rb
:
def to_db_hash
# вызываем родительский метод ключевым словом super и к хэшу, который он вернул
# присоединяем прицепом специфичные для этого класса поля методом Hash#merge
return super.merge(
{
'text' => @text,
'url' => @url
}
)
end
# загружаем свои поля из хэш массива
def load_data(data_hash)
super(data_hash) # сперва дергаем родительский метод для общих полей
# теперь прописываем свое специфичное поле
@url = data_hash['url']
end
memo.rb
:
def to_db_hash
# вызываем родительский метод ключевым словом super и к хэшу, который он вернул
# присоединяем прицепом специфичные для этого класса поля методом Hash#merge
return super.merge(
{
'text' => @text.join('\n\r') # массив строк делаем одной большой строкой, разделенной символами перевода строки
}
)
end
# загружаем свои поля из хэш массива
def load_data(data_hash)
super(data_hash) # сперва дергаем родительский метод для общих полей
# теперь прописываем свое специфичное поле
@text = data_hash['text'].split('\n\r')
end
task.rb
:
def to_db_hash
# вызываем родительский метод ключевым словом super и к хэшу, который он вернул
# присоединяем прицепом специфичные для этого класса поля методом Hash#merge
return super.merge(
{
'text' => @text,
'due_date' => @due_date.to_s
}
)
end
# загружаем свои поля из хэш массива
def load_data(data_hash)
super(data_hash) # сперва дергаем родительский метод для общих полей
# теперь прописываем свое специфичное поле
@due_date = Date.parse(data_hash['due_date'])
end
Перепишем основную программу для создания записи new_post.rb
:
# Подключаем класс Post и его детей
require_relative 'post.rb'
require_relative 'memo.rb'
require_relative 'link.rb'
require_relative 'task.rb'
# Как обычно, при использовании классов программа выглядит очень лаконично
puts "Привет, я блокнот версия 2, записываю новые записи в базу SQLite :)"
# Теперь надо спросить у пользователя, что он хочет создать
puts "Что хотите записать сегодня?"
# массив возможных видов Записи (поста)
choices = Post.post_types.keys
choice = -1
until choice >= 0 && choice < choices.size # пока юзер не выбрал правильно
# выводим заново массив возможных типов поста
choices.each_with_index do |type, index|
puts "\t#{index}. #{type}"
end
choice = gets.chomp.to_i
end
# выбор сделан, создаем запись с помощью стат. метода класса Post
entry = Post.create(choices[choice])
# Просим пользователя ввести пост (каким бы он ни был)
entry.read_from_console
# Сохраняем пост в базу данных
rowid = entry.save_to_db
puts "Запись сохранена в базе, id = #{rowid}"
Чтобы метод Post.find
получил нужные параметры, ему их нужно передать, давайте по-взрослому будем прокидывать их из параметров командной строки. Снова следите за мыслью при написании основной программы в файле read.rb
:
# Подключаем класс Post и его детей
require_relative 'post.rb'
require_relative 'memo.rb'
require_relative 'link.rb'
require_relative 'task.rb'
# будем обрабатывать параметры командной строки по-взрослому с помощью спец. библиотеки руби
require 'optparse'
# Все наши опции будут записаны сюда
options = {}
# заведем нужные нам опции
OptionParser.new do |opt|
opt.banner = 'Usage: read.rb [options]'
opt.on('-h', 'Prints this help') do
puts opt
exit
end
opt.on('--type POST_TYPE', 'какой тип постов показывать (по умолчанию любой)') { |o| options[:type] = o } #
opt.on('--id POST_ID', 'если задан id — показываем подробно только этот пост') { |o| options[:id] = o } #
opt.on('--limit NUMBER', 'сколько последних постов показать (по умолчанию все)') { |o| options[:limit] = o } #
end.parse!
result = Post.find(options[:limit], options[:type], options[:id])
if result.is_a? Post # показываем конкретный пост
puts "Запись #{result.class.name}, id = #{options[:id]}"
# выведем весь пост на экран и закроемся
result.to_strings.each do |line|
puts line
end
else # показываем таблицу результатов
print "| id\t| @type\t| @created_at\t\t\t| @text \t\t\t| @url\t\t| @due_date \t "
result.each do |row|
puts
# puts '_'*80
row.each do |element|
print "| #{element.to_s.delete("\\n\\r")[0..40]}\t"
end
end
end
puts
# Фигурные скобки {...} после вызова метода в простых случаях аналогичны конструкции do ... end
# Они ограничивают блок кода который будет выполняться этим методом
#
# см. http://stackoverflow.com/questions/5587264/do-end-vs-curly-braces-for-blocks-in-ruby
Теперь можно создать пару записей и посмотреть в нашем SQlite Manager-е на результат:
Итак, мы разобрались с базами данных и научились использовать систему управления базами данных sqlite в нашем блокноте. Конечно, программа далека от идеала: нельзя, например, удалять записи, нельзя редактировать посты. Но для иллюстрации работы с базой данных этого достаточно.