ActiveRecord

Большая, удобная и достаточно продуманная ORM, которая так же используется в Rails. Но никто не запрещает использовать gem отдельно от фреймворков. Здесь я попробую описать то, в чём я успел разобраться.

Установка

Зависимостей вроде не тянет, так что

gem install activerecord

Инициализация

ActiveRecord поддерживает подавляющее большинство бэкендов БД. Я использовал SQLite31) и PostgreSQL2) Собственно первый шаг к мировому господству - настроить подключение к БД

ActiveRecord::Base.establish_connection(
  :adapter => 'postgresql',
  :user => '<username>',
  :password => '<password>',
  :host => '<hostname>',
  :database => '<database_name>',
  :encoding => 'utf8',
  :pool => 100500
)

Что же тут покручено

  • adapter - непосредственно указание адаптера БД (postgresql, sqlite3, mysql, etc)
  • user - пользователь, которым будет произведена авторизация
  • password - пароль, с помощью которого будет производиться авторизация
  • host - адрес сервера БД
  • database - название БД на сервер
  • encoding - кодировка подключения (я не знаю почему, но некоторые админы настраивают локали очень странно, вплоть до latin-1 и windows-1251)
  • pool - максимальное количество подключений к БД

Для SQLite3 есть свои педальки

ActiveRecord::Base.establish_connection(
  :adapter => 'sqlite3',
  :dbfile => '<path_to_file>'
)

Создание схемы БД

Производится посредством передачи блока методу define класса ActiveRecord::Schema. В нашем распоряжении есть пачка методов, которые позволяют достаточно гибко обхявить схему БД.

  • create_table :table_name &block - наверное самый распростанённый метод, используется для непосредственно обхявления таблицы
  • create_join_table :left_table, :right_table &block - служит для создания кросс-таблицы при использовании в схеме many-to-many

Внутри блока, передаваемого в методы, можно использовать следующие типы полей

  • string - varchar
  • text - текст
  • integer - числовой
  • timestamps - особый тип, создаёт сразу два поля - updated_at, created_at
  • index - создаёт индекс для поля, можно передать тип индекса (unique, например)

Дополнительно здесь же можно описать связи таблиц:

  • belongs_to - указывает на связь один к одному (указывается в подчинённой таблице, служит для создания <master_table>_id поля)
  • has_many - указывает на связь один ко многим
  • has_one - указывает на связь один к одному (как belongs_to, только немного другой)
  • has_many - указывает на связь много ко многим (создаётся через связующую модель, хотя у меня получилось и без неё)

Ну и, как водится, пример

ActiveRecord::Schema.define do
  create_table :authors do |t|
    t.string :firstname, default: ''
    t.string :lastname, default: ''
    t.string :surname, default: ''
    t.string :search_hash
    t.index :search_hash, unique: true
  end unless ActiveRecord::Base.connection.table_exists? :authors
 
  create_table :genres do |t|
    t.references :parents, index: true
    t.string :genre
    t.string :code
    t.index :code, unique: true
  end unless ActiveRecord::Base.connection.table_exists? :genres
 
  create_table :series do |t|
    t.string :serie
    t.index :serie, unique: true
  end unless ActiveRecord::Base.connection.table_exists? :series
 
  create_table :books do |t|
    t.belongs_to :serie, index: true
    t.integer :serie_pos, default: 0
    t.string :title
    t.integer :size
    t.string :search_hash
    t.string :format
    t.string :lang
    t.string :import_file
    t.timestamps
    t.index :search_hash, unique: true
  end unless ActiveRecord::Base.connection.table_exists? :books
 
  create_join_table :genres, :books do |t|
    t.index :genre_id
    t.index :book_id
  end unless ActiveRecord::Base.connection.table_exists? :books_genres
 
  create_join_table :authors, :books do |t|
    t.index :author_id
    t.index :book_id
  end unless ActiveRecord::Base.connection.table_exists? :authors_books
end

Поскольку в данном проекте я не использую Rails, и ещё пока не особо проникся миграциями - я сделал проще. Если таблицы нет - создать.

Связи между таблицами

Связи не обязательно определять в схеме БД3), однако в моделях без этого не обойтись, иначе ORM будет вести себя как минимум странно.

Из теории БД4), мы знаем, что способов связи банных не так много - один к одному, когда одна запись в левой таблице соответствет только одной записи в правой, один ко многим - когда одна запись в левой таблице соответствует нескольким записям в правой и много ко многим, когда несколько записей в левой таблице соответствуют нескольким записям в правой таблице. Последний вариант самый сложный и делается через промежуточную таблицу, но обо всё по порядку.

Один к одному

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

one2one.rb
require 'active_record'
 
ActiveRecord::Base.establish_connection(
  :adapter => 'sqlite3',
  :database => 'customers.sqlite3'
)
 
ActiveRecord::Schema.define do
  create_table :customers do |t|
    t.string :firstname
    t.string :lastname
    t.string :surname, default: ''
  end unless ActiveRecord::Base.connection.table_exists? :customers
 
  create_table :phones do |t|
    t.string :provider
    t.string :number
    t.belongs_to :customer
  end unless ActiveRecord::Base.connection.table_exists? :phones
end
 
class Customer < ActiveRecord::Base
  has_one :phone
end
 
class Phone < ActiveRecord::Base
   belongs_to :customer
end
 
customer = Customer.new do |c|
  c.firstname = 'Иван'
  c.lastname = 'Иванов'
  c.surname = 'Иванович'
end
customer.save
 
phone = Phone.new do |p|
  p.provider = 'Мегафон'
  p.number = '+79261234567'
  p.customer = customer
end
phone.save

После выполнения получаем БД следующего вида:

customers
id firstname lastname surname
1 Иван Иванов Иванович
phones
id provider number customer_id
1 Мегафон +7926123457 1

Один ко многим

Пожалуй самый распространённый способ связи таблиц в БД. Идея простая - одна запись из левой таблицы может соответствовать нескольким записям из правой таблицы и наоборот, но не одновременно.

За пример возьмём предыдущую, базу, но теперь приблизим её немного к реальности, а именно - у одного человека может быть несколько телефонных номеров, посему сама схема особо не изменится, а вот модели станут немного другими:

one2many.rb
require 'active_record'
 
ActiveRecord::Base.establish_connection(
  :adapter => 'sqlite3',
  :database => 'customers.sqlite3'
)
 
ActiveRecord::Schema.define do
  create_table :customers do |t|
    t.string :firstname
    t.string :lastname
    t.string :surname, default: ''
  end unless ActiveRecord::Base.connection.table_exists? :customers
 
  create_table :phones do |t|
    t.string :provider
    t.string :number
    t.belongs_to :customer
  end unless ActiveRecord::Base.connection.table_exists? :phones
end
 
class Customer < ActiveRecord::Base
  has_many :phone
end
 
class Phone < ActiveRecord::Base
  belongs_to :customer
end
 
customer = Customer.new do |c|
  c.firstname = 'Иван'
  c.lastname = 'Иавнов'
  c.surname = 'Иванович'
end
customer.save
 
phones = []
phones << Phone.new do |p|
  p.provider = 'Мегафон'
  p.number = '+79261234567'
  p.customer = customer
end
 
phones << Phone.new do |p|
  p.provider = 'Билайн'
  p.number = '+79671234567'
  p.customer = customer
end
 
phones.each do |p|
  p.save
end

В базе же ничего не изменится. Здесь разница скорее в логике работы моделей, нежели в структуре БД.

Много ко многим

В своей жизни я встречаюсь с этим типом связей слишком часто5). Суть связи в том, что несколько записей из левой таблице могут быть одновременно связаны с несколькими записями в правой таблице.

Возьмём пример выше, где несколько книг могут иметь несколько писателей.

many2many.rb
require 'active_record'
 
ActiveRecord::Base.establish_connection(
  :adapter => 'sqlite3',
  :database => 'customers.sqlite3'
)
 
ActiveRecord::Schema.define do
  create_table :authors do |t|
    t.string :firstname, default: ''
    t.string :lastname, default: ''
    t.string :surname, default: ''
  end unless ActiveRecord::Base.connection.table_exists? :authors
 
  create_table :books do |t|
    t.string :title
    t.timestamps
  end unless ActiveRecord::Base.connection.table_exists? :books
 
  create_join_table :authors, :books do |t|
    t.index :author_id
    t.index :book_id
  end unless ActiveRecord::Base.connection.table_exists? :authors_books
end
 
class Book < ActiveRecord::Base
  has_and_belongs_to_many :authors
end
 
class Author < ActiveRecord::Base
  has_and_belongs_to_many :books
end
 
books = []
books << Book.new do |b|
  b.title = 'Фантастика для самых маленьких'
end
 
books << Book.new do |b|
  b.title = 'Фантатстика для побольше'
end
 
books << Book.new do |b|
  b.title = 'Совершенно непонятная фантастика'
end
 
books.each do |b|
  b.save
end
 
authors = []
authors << Author.new do |a|
  a.firstname = 'Аркадий'
  a.lastname = 'Аркадьев'
  a.surname = 'Аркадьевич'
end
 
authors << Author.new do |a|
  a.firstname = 'Иван'
  a.lastname = 'Иванов'
end
 
authors << Author.new do |a|
  a.firstname = 'Александр'
  a.lastname = 'Александров'
end
 
authors.each do |a|
  a.save
end
 
books[0].authors = [ authors[0] ]
books[0].save
 
books[1].authors = authors
books[1].save
 
books[2].authors = authors[1..2]
books[2].save

На выходе получае три таблицы:

books
id title created_at updated_at
1 Фантастика для самых маленьких 2017-11-18 19:34:10.937070 2017-11-18 19:34:10.937070
2 Фантатстика для побольше 2017-11-18 19:34:11.008798 2017-11-18 19:34:11.008798
3 Совершенно непонятная фантастика 2017-11-18 19:34:11.086593 2017-11-18 19:34:11.086593
authors
id firstname lastname surname
1 Аркадий Аркадьев Аркадьевич
2 Иван Иванов
3 Александр Александров
authors_books
author_id bookd_id
1 1
1 2
2 2
3 2
2 3
3 3
1)
и осознал, что её надо либо держать на ssd или в ram, либо лучше не использовать для сложных задач
2)
а этот ничего так, бодренько себя показал
3)
о чём свидетельствует пример выше
4)
кто читал
5)
возможно я слишком щепетилен к деталям