Программирование на языке Ruby.

10.4. Подключение к внешним базам данных.

Благодаря усилиям многих людей Ruby может взаимодействовать с разными базами данных, от монолитных систем типа Oracle до более скромного MySQL. Для полноты описания мы включили в него также текстовые файлы в формате CSV.

Уровень функциональности, реализованный в этих пакетах, постоянно изменяется. Обязательно познакомьтесь с последней версией документации в сети. Неплохой отправной точкой станет архив приложений Ruby.

10.4.1. Интерфейс с SQLite.

SQLite — популярная база данных для тех, кто ценит программное обеспечение, которое не нужно конфигурировать. Это небольшая автономная исполняемая программа, написанная на языке С, которая хранит всю базу данных в одном файле. Хотя обычно она используется для небольших баз, но теоретически способна управиться с терабайтными объемами.

Привязка Ruby к SQLite довольно прямолинейна. API, написанный на С, обернут в класс SQLite::API. Поскольку при этом методы отображаются один в один и интерфейс не назовешь образцом объектной ориентированности, пользоваться этим API стоит только в случае острой необходимости.

В большинстве ситуаций вам будет достаточно класса SQLite::Database. Вот пример кода:

Require 'sqlite'.

Db = SQLite::Database.new("library.db").

Db.execute("select title,author from books") do |row|

 p row.

End.

Db.close.

# Выводится:

# ["The Case for Mars", "Robert Zubrin"].

# ["Democracy in America", "Alexis de Tocqueville"].

# ...

Если блок не задан, то метод execute возвращает объект ResultSet (по сути, курсор, который можно перемещать по набору записей).

Rs = db.execute("select title,author from books").

Rs.each {|row| p row } # Тот же результат, что и выше.

Rs.close.

Если получен объект ResultSet, то программа должна будет рано или поздно закрыть его (как показано в примере выше). Если нужно обойти список записей несколько раз, то с помощью метода reset можно вернуться в начало. (Это экспериментальное средство, которое в будущем может измениться.) Кроме того, можно производить обход в духе генератора с помощью методов next и eof?.

Rs = db.execute("select title,author from books").

While ! rs.eof?

 rec = rs.next.

 p rec # Тот же результат, что и выше.

End.

Rs.close.

Методы библиотеки могут возбуждать различные исключения. Все они являются подклассами класса SQLite::Exception, так что легко перехватываются поодиночке или целой группой.

Отметим еще, что библиотека написана так, что может работать совместно с библиотекой ArrayFields Ары Ховарда (Ara Howard). Она позволяет получать доступ к элементам массива по индексу или по имени. Если перед sqlite затребована библиотека arrayfields, то объект ResultSet можно индексировать как числами, так и именами полей. (Но можно задать и такую конфигурацию, что вместо этого будет возвращаться объект Hash.)

Хотя библиотека sqlite вполне развита, она не покрывает всех мыслимых потребностей просто потому, что сама база данных SQLite не полностью реализует стандарт SQL92. Дополнительную информацию об SQLite и привязке к Ruby ищите в сети.

10.4.2. Интерфейс с MySQL.

Интерфейс Ruby с MySQL — один из самых стабильных и полнофункциональных среди всех интерфейсов с базами данных. Это расширение, которое должно устанавливаться после инсталляции Ruby и MySQL.

Для использования модуля нужно выполнить три шага: прежде всего, загрузить модуль в свой сценарий, затем установить соединение с базой данных и, наконец, начать работать с таблицами. Для установления соединения следует задать обычные параметры: имя хоста, имя пользователя, пароль, имя базы данных и т.д.

Require 'mysql'.

M = Mysql.new("localhost","ruby","secret","maillist").

R = m.query("SELECT * FROM people ORDER BY name").

R.each_hash do |f|

 print "#{f['name']} - #{f['email']}".

End.

# Выводится что-то вроде:

# John Doe - jdoe@rubynewbie.com.

# Fred Smith - smithf@rubyexpert.com.

Особенно полезны методы класса Mysql.new и MysqlRes.each_hash, а также метод экземпляра query.

Модуль состоит из четырех классов ( Mysql, MysqlRes, MysqlField и MysqlError), описанных в файле README. Мы приведем сводку некоторых наиболее употребительных методов, а дополнительную информацию вы сможете найти сами в официальной документации.

Метод класса Mysql.new принимает несколько строковых параметров, которые по умолчанию равны nil, и возвращает объект, представляющий соединение. Параметры называются host, user, passwd, db, port, sock и flag. У метода new есть синонимы real_connect и connect.

Методы create_db, select_db и drop_db принимают в качестве параметров имя базы данных и используются, как показано ниже. Метод close закрывает соединение с сервером.

M=Mysql.new("localhost","ruby","secret").

M.create_db("rtest")  # Создать новую базу данных.

M.select_db("rtest2") # Выбрать другую базу данных.

In.drop_db("rtest")   # Удалить базу данных.

M.close               # Закрыть соединение.

В последних версиях методы create_db и drop_db объявлены устаревшими. Но можно «воскресить» их, определив следующим образом:

Class Mysql.

 def create_db(db).

  query("CREATE DATABASE #{db}").

 end.

 def drop_db(db).

  query("DROP DATABASE #{db}").

 end.

End.

Метод list_dbs возвращает список имен доступных баз данных в виде массива.

Dbs = m.list_dbs # ["people","places","things"].

Метод query принимает строковый параметр и по умолчанию возвращает объект MysqlRes. В зависимости от заданного значения свойства query_with_result может также возвращаться объект Mysql.

Если произошла ошибка, то ее номер можно получить, обратившись к методу errno. Метод error возвращает текст сообщения об ошибке.

Begin.

 r=m.query("create table rtable.

 (

  id int not null auto_increment,

  name varchar(35) not null,

  desc varchar(128) not null,

  unique id(id).

 )").

# Произошло исключение...

Rescue.

 puts m.error.

  # Печатается: You have an error in your SQL syntax.

  # near 'desc varchar(128) not null ,

  # unique id(id).

  # )' at line 5".

 puts m.errno.

  # Печатается 1064.

  # ('desc' is reserved for descending order).

End.

Ниже перечислено несколько полезных методов экземпляра, определенных в классе MysqlRes:

•  fetch_fields возвращает массив объектов MysqlField, соответствующих полям в следующей строке;

•  fetch_row возвращает массив значений полей в следующей строке;

•  fetch_hash(with_table=false) возвращает хэш, содержащий имена и значения полей в следующей строке;

•  num_rows возвращает число строк в результирующем наборе;

•  each — итератор, последовательно возвращающий массив значений полей;

•  each_hash(with_table=false) — итератор, последовательно возвращающий хэш вида {имя_поля => значение_поля} (пользуйтесь нотацией x['имя_поля'] для получения значения поля).

Вот некоторые методы экземпляра, определенные в классе MysqlField:

•  name возвращает имя поля;

•  table возвращает имя таблицы, которой принадлежит поле;

•  length возвращает длину поля, заданную при определении таблицы;

•  max_length возвращает длину самого длинного поля в результирующем наборе;

•  hash возвращает хэш с именами и значениями следующих элементов описания: name, table, def, type, length, max_length, flags, decimals.

Если изложенный здесь материал противоречит онлайновой документации, предпочтение следует отдать документации. Более подробную информацию вы найдете на официальном сайте MySQL (http://www.mysql.com) и в архиве приложений Ruby.

10.4.3. Интерфейс с PostgreSQL.

В архиве RAA есть также расширение, реализующее доступ к СУБД PostgreSQL (работает с версиями PostgreSQL 6.5/7.0).

В предположении, что PostgreSQL уже установлена и сконфигурирована (и в базе данных есть таблица testdb), нужно лишь выполнить те же шаги, что и для всех остальных интерфейсов Ruby с базами данных: загрузить модуль, установить соединение с базой данных и начать работу. Надо полагать, вам понадобится способ послать запрос, получить результаты и работать с транзакциями.

Require 'postgres'.

Conn = PGconn.connect("", 5432, "", "", "testdb").

Conn.exec("create table rtest ( number integer default 0 );").

Conn.exec("insert into rtest values ( 99 )").

Res = conn.query("select * from rtest").

# res id [["99"]].

В классе PGconn есть метод connect, который принимает обычные параметры для установления соединения: имя хоста, номер порта, имя базы данных, имя и пароль пользователя. Кроме того, третий и четвертый параметры — соответственно, флаги и параметры терминала. В приведенном примере мы установили соединение через сокет UNIX от имени привилегированного пользователя, поэтому не указывали ни имя пользователя, ни пароль, а имя хоста, флаги и параметры терминала оставили пустыми. Номер порта должен быть целым числом, а остальные параметры — строками. У метода connect есть синоним new.

Для работы с таблицами нужно уметь выполнять запросы. Для этого служат методы PGconn#exec и PGconn#query.

Метод exec посылает переданную ему строку — SQL-запрос — серверу PostgreSQL и получает ответ в виде объекта PGresult, если выполнение завершилось успешно. В противном случае он возбуждает исключение PGError.

Метод query также посылает свой строковый параметр в виде SQL-запроса. Но в случае успеха получает массив кортежей. В случае ошибки возвращается nil, а подробности можно получить, вызвав метод error.

Имеется специальный метод insert_table для вставки записи в указанную таблицу. Вопреки названию он не создает новую таблицу, а добавляет данные в существующую. Этот метод возвращает объект PGconn.

Conn.insert_table("rtest",[[34]]).

Res = conn.query("select * from rtest").

Res равно [["99"], ["34"]].

В этом примере в таблицу rtest вставляется одна строка. Для простоты мы указали только одну колонку. Отметим, что объект res класса PGresult после обновления возвращает массив из двух кортежей. Чуть ниже мы рассмотрим методы, определенные в классе PGresult.

В классе PGconn определены также следующие полезные методы:

•  db возвращает имя базы, с которой установлено соединение;

•  host возвращает имя сервера, с которым установлено соединение;

•  user возвращает имя аутентифицированного пользователя;

•  error возвращает сообщение об ошибке;

•  finish, close закрывают соединение;

•  loimport(file) импортирует файл в большой двоичный объект (BLOB), в случае успеха возвращает объект PGlarge, иначе возбуждает исключение PGError;

•  loexport(oid, file) выгружает BLOB с идентификатор oid в указанный файл;

•  locreate([mode]) возвращает объект PGlarge в случае успеха, иначе возбуждает исключение PGError;

•  loopen(oid, [mode]) открывает BLOB с идентификатором oid. Возвращает объект PGlarge в случае успеха. Аргумент mode задает режим работы с открытым объектом: "INV_READ" или "INV_WRITE" (если этот аргумент опущен, по умолчанию предполагается "INV_READ");

•  lounlink(oid) удаляет BLOB с идентификатором oid.

Отметим, что пять последних методов ( loimport, loexport, locreate, loopen и lounlink) работают с объектами класса PGlarge. У этого класса есть собственные методы для доступа к объекту и его изменения. (ВLОВ'ы создаются в результате выполнения методов loimport, locreate, loopen экземпляра.).

Ниже перечислены методы, определенные в классе PGlarge:

•  open([mode]) открывает BLOB. Аргумент mode задает режим работы с объектом, как и в случае с методом PGconn#loopen);

•  close закрывает BLOB (ВLОВ'ы также закрываются автоматически, когда их обнаруживает сборщик мусора);

•  read([length]) пытается прочитать length байтов из BLOB'a. Если параметр length не задан, читаются все данные;

•  write(str) записывает строку в BLOB и возвращает число записанных байтов;

•  tell возвращает текущую позицию указателя;

•  seek(offset, whence) перемещает указатель в позицию offset. Параметр whence может принимать значения SEEK_SET, SEEK_CUR и SEEK_END (равные соответственно 0,1,2);

•  unlink удаляет BLOB;

•  oid возвращает идентификатор BLOB'a;

•  size возвращает размер BLOB'a;

•  export(file) сохраняет BLOB в файле с указанным именем.

Более интересны методы экземпляра, определенные в классе PGresult (перечислены ниже). Объект такого класса возвращается в результате успешного выполнения запроса. (Для экономии памяти вызывайте метод PGresult#clear по завершении работы с таким объектом.).

•  result возвращает массив кортежей, описывающих результат запроса;

•  each — итератор;

•  [] — метод доступа;

•  fields возвращает массив описаний полей результата запроса;

•  num_tuples возвращает число кортежей в результате запроса;

•  fieldnum(name) возвращает индекс поля с указанным именем;

•  type(index) возвращает целое число, соответствующее типу поля;

•  size(index) возвращает размер поля в байтах. 1 означает, что поле имеет переменную длину;

•  getvalue(tup_num, field_num) возвращает значение поля с указанным порядковым номером; tup_num — номер строки;

•  getlength(tup_num, field_num) возвращает длину поля в байтах;

•  cmdstatus возвращает строку состояния для последнего запроса;

•  clear очищает объект PGresult.

10.4.4. Интерфейс с LDAP.

Для Ruby есть по меньшей мере три разных библиотеки, позволяющих работать с протоколом LDAP. Ruby/LDAP, написанная Такааки Татеиси (Takaaki Tateishi), — это довольно «тонкая» обертка. Если вы хорошо знакомы с LDAP, то ее может оказаться достаточно; в противном случае вы, наверное, сочтете ее слишком сложной. Пример:

Conn = LDAP::Conn.new("rsads02.foo.com").

Conn.bind("CN=username,CN=Users,DC=foo,DC=com", "password") do |bound|

 bound.search("DC=foo,DC=com", LDAP::LDAP_SCOPE_SUBTREE,

  "(&(name=*) (objectCategory=person))", ['name','ipPhone']).

 do |user|

  puts "#{user['name']} #{user['ipPhone']}".

 end.

End.

Библиотека ActiveLDAP организована по образцу ActiveRecord. Вот пример ее использования, взятый с домашней страницы:

Require 'activeldap'.

Require 'examples/objects/user'.

Require 'password'.

# Установить соединение Ruby/ActiveLDAP и т. д.

ActiveLDAP::Base.connect(:password_block.

 => Proc.new { Password.get('Password: ') },

  :allow_anonymous => false).

# Загрузить запись с данными о пользователе.

# (ее класс определен в примерах).

Wad = User.new('wad').

# Напечатать общее имя.

Р wad.cn.

# Изменить общее имя.

Wad.cn = "Will".

# Сохранить в LDAP.

Wad.write.

Есть также сравнительно недавняя библиотека, написанная Фрэнсисом Чианфрокка (Francis Cianfrocca), многие предпочитают именно ее:

Require 'net/ldap'.

Ldap = Net::LDAP.new :host => server_ip_address,

 :port => 389,

 :auth => {

  :method => :simple,

  :username => "cn=manager,dc=example,dc=com",

  :password => "opensesame".

 }

Filter = Net::LDAP::Filter.eq( "cn", "George*" ).

Treebase = "dc=example,dc=com".

Ldap.search( :base => treebase, :filter => filter ) do |entry|

 puts "DN: #{entry.dn}".

 entry.each do |attribute, values|

  puts " #{attribute}:".

  values.each do |value|

   puts " --->#{value}".

  end.

 end.

End.

P ldap.get_operation_result.

Какая из этих библиотек лучше — дело вкуса. Я рекомендую познакомиться со всеми и сформировать собственное мнение.

10.4.5. Интерфейс с Oracle.

Oracle — одна из наиболее мощных и популярных СУБД в мире. Понятно, что было много попыток реализовать интерфейс с этой базой данных из Ruby. На сегодняшний день лучшей считается библиотека OCI8, которую написал Кубо Такехиро (Kubo Takehiro).

Вопреки названию, библиотека OCI8 работает и с версиями Oracle младше 8. Но она еще не вполне зрелая, поэтому не позволяет воспользоваться некоторыми средствами, появившимися в последних версиях.

API состоит из двух уровней: тонкая обертка (низкоуровневый API, довольно точно повторяющий интерфейс вызовов Oracle — Call Level Interface). Но в большинстве случаев вы будете работать с высокоуровневым API. Не исключено, что в будущем низкоуровневый API станет недокументированным.

Модуль OCI8 включает классы Cursor и Blob. Класс OCIException служит предком всех классов исключений, которые могут возникнуть при работе с базой данных: OCIError, OCIBreak и OCIInvalidHandle.

Чтобы установить соединение с сервером, вызывается метод OCI8.new, которому нужно передать как минимум имя и пароль пользователя. В ответ возвращается описатель, который можно использовать для выполнения запросов. Пример:

Require 'oci8'.

Session = OCI8.new('user', 'password').

Query = "SELECT TO_CHAR(SYSDATE, 'YYYY/MM/DD') FROM DUAL".

Cursor = session.exec(query).

Result = cursor.fetch # В данном случае всего одна итерация.

Cursor.close.

Session.logoff.

В примере выше показано, как манипулировать курсором, хотя в данном случае перед закрытием выполняется всего одна операция fetch. Конечно, можно выбрать и несколько строк:

Query = 'select * from some_table'.

Cursor = session.exec(query).

While row = cursor.fetch.

 puts row.join(",").

End.

Cursor.close.

# Или с помощью блока:

Nrows = session.exec(query) do |row|

 puts row.join(",").

End.

Связанные переменные в запросе напоминают символы. Есть несколько способов связать переменные со значениями:

Session = OCI8.new("user","password").

Query = "select * from people where name = :name".

# Первый способ...

Session.exec(query,'John Smith').

# Второй...

Cursor = session.parse(query).

Cursor.exec('John Smith').

# Третий...

Cursor = session.parse(query).

Cursor.bind_param(':name','John Smith') # Связывание по имени.

Cursor.exec.

# И четвертый.

Cursor = session.parse(query).

Cursor.bind_param(1,'John Smith')       # Связывание по номеру.

Cursor.exec.

Для тех, кто предпочитает интерфейс DBI, имеется соответствующий адаптер. Дополнительную информацию можно найти в документации по OCI8.

10.4.6. Обертка вокруг DBI.

Теоретически интерфейс DBI обеспечивает доступ к любым базам данных. Иными словами, один и тот же код должен работать и с Oracle, и с MySQL, и с PostgreSQL, и с любой другой СУБД, стоит лишь изменить одну строку, в которой указан нужный адаптер. Иногда эта идеология не срабатывает для сложных операций, специфичных для конкретной СУБД, но для рутинных задач она вполне годится.

Пусть имеется база данных под управлением Oracle и используется драйвер (он же адаптер), поставляемый вместе с библиотекой OCI8. Методу connect следует передать достаточно информации для успешного соединения с базой данных. Все более или менее интуитивно очевидно.

Require "dbi".

Db = DBI.connect("dbi:OCI8:mydb", "user", "password").

Query = "select * from people".

Stmt = db.prepare(query).

Stmt.execute.

While row = stmt.fetch do.

 puts row.join(",").

End.

Stmt.finish.

Db.disconnect.

Здесь метод prepare — это некий вариант компиляции или синтаксического анализа запроса, который позже исполняется. Метод fetch извлекает одну строку из результирующего набора и возвращает nil, если строк не осталось (поэтому мы и воспользовались циклом while). Метод finish можно считать вариантом закрытия или освобождения ресурсов.

Полную информацию обо всех возможностях DBI можно найти в любом справочном руководстве. Список имеющихся драйверов приведен на сайте RubyForge и в архиве приложений Ruby.

10.4.7. Объектно-реляционные отображения (ORM).

Традиционная реляционная база данных прекрасно справляется со своими задачами. Она эффективно выполняет произвольные запросы, о которых заранее ничего не знает. Но эта модель плохо уживается с объектной ориентированностью.

Повсеместная распространенность обеих моделей (РСУБД и ООП) и «несогласованный импеданс» между ними побудил многих людей попытаться перебросить мост. Этот программный мост получил название «объектно-реляционное отображение» (Object-Relational Mapper — ORM).

К этой задаче существуют разные подходы. У каждого есть свои достоинства и недостатки. Ниже мы рассмотрим два популярных ORM: ActiveRecord и Og (последняя аббревиатура обозначает «object graph» — граф объектов).

Библиотека ActiveRecord для Ruby названа в честь предложенного Мартином Фаулером (Martin Fowler) паттерна проектирования «Active Record» (активная запись). Смысл его в том, что таблицам базы данных сопоставляются классы, в результате чего данными становится возможно манипулировать без привлечения SQL. Точнее говоря, «она (активная запись) обертывает строку таблицы или представления, инкапсулирует доступ к базе данных и наделяет данные логикой, присущей предметной области» (см. книгу Martin Fowler «Patterns of Enterprise Application Architecture», Addison Wesley, 2003 [ISBN: 0-321-12742-0e]).

Каждая таблица описывается классом, производным от ActiveRecord::Base. Как и в случае с DBI, для установления соединения нужно предоставить достаточно информации для идентификации пользователя и базы данных. Вот небольшой пример, демонстрирующий весь механизм в действии:

Require 'active_record'.

ActiveRecord::Base.establish_connection(:adapter => "oci8",

 :username => "username",

 :password => "password",

 :database => "mydb",

 :host => "myhost").

Class SomeTable < ActiveRecord::Base.

 set_table_name "test_table".

 set_primary_key "some_id".

End.

SomeTable.find(:all).each do |rec|

 # Обработать запись...

End.

Item = SomeTable.new.

Item.id = 1001.

Item.some_column = "test".

Item.save.

Библиотека предлагает богатый и сложный API. Я рекомендую ознакомиться со всеми руководствами, которые вы сможете найти в сети или в книгах. Поскольку эта библиотека составляет неотъемлемую часть системы «Ruby on Rails», то мы еще вернемся к ней в главе, посвященной этой теме.

Og отличается от ActiveRecord тем, что в центре внимания последней находится база данных, а первая делает упор на объекты, Og может сгенерировать схему базы данных, имея определения классов на языке Ruby (но не наоборот).

При работе с Og нужен совсем другой стиль мышления; она не так распространена, как ActiveRecord. Но мне кажется, что у этой библиотеки есть свои «изюминки», и ее следует рассматривать как мощный и удобный механизм ORM, особенно если вы проектируете базу данных исходя из структуры имеющихся объектов.

Определяя подлежащий хранению класс, мы пользуемся методом property, который похож на метод attr_accessor, только с ними ассоциирован тип (класс).

Class SomeClass.

 property :alpha, String.

 property :beta, String.

 property :gamma, String.

End.

Поддерживаются также типы данных Integer, Float, Time, Date и пр. Потенциально возможно связать со свойством произвольный объект Ruby.

Соединение с базой данных устанавливается так же, как в случае ActiveRecord или DBI.

Db = Og::Database.new(:destroy => false,

 :name => 'mydb',

 :store => :mysql,

 :user => 'hal9000',

 :password => 'chandra').

У каждого объекта есть метод save, который и вставляет соответствующую ему запись в базу данных:

Obj = SomeClass.new.

Obj.alpha = "Poole".

Obj.beta = "Whitehead".

Obj.gamma = "Kaminski".

Obj.save.

Имеются также методы для описания связей объекта в терминах классической теории баз данных:

Class Dog.

 has_one    :house.

 belongs_to :owner.

 has_many   :fleas.

End.

Эти, а также другие методы, например many_to_many и refers_to, помогают создавать сложные связи между объектами и таблицами.

Библиотека Og слишком велика, чтобы ее документировать на страницах этой книги. Дополнительную информацию вы можете найти в онлайновых источниках (например, на сайте http://oxyliquit.de).