Большая, удобная и достаточно продуманная 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 )
Что же тут покручено
Для SQLite3 есть свои педальки
ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :dbfile => '<path_to_file>' )
Производится посредством передачи блока методу define класса ActiveRecord::Schema. В нашем распоряжении есть пачка методов, которые позволяют достаточно гибко обхявить схему БД.
Внутри блока, передаваемого в методы, можно использовать следующие типы полей
Дополнительно здесь же можно описать связи таблиц:
Ну и, как водится, пример
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), мы знаем, что способов связи банных не так много - один к одному, когда одна запись в левой таблице соответствет только одной записи в правой, один ко многим - когда одна запись в левой таблице соответствует нескольким записям в правой и много ко многим, когда несколько записей в левой таблице соответствуют нескольким записям в правой таблице. Последний вариант самый сложный и делается через промежуточную таблицу, но обо всё по порядку.
Мне сложно представить себе реальную ситуацию, в которой может пригодиться данный тип связи. Может для разделения слишком широкой таблицы на несколько. Придумаем, например, телефонный справочник, в котором только одному человеку может принадлежать телефонный номер.
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 |
Пожалуй самый распространённый способ связи таблиц в БД. Идея простая - одна запись из левой таблицы может соответствовать нескольким записям из правой таблицы и наоборот, но не одновременно.
За пример возьмём предыдущую, базу, но теперь приблизим её немного к реальности, а именно - у одного человека может быть несколько телефонных номеров, посему сама схема особо не изменится, а вот модели станут немного другими:
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). Суть связи в том, что несколько записей из левой таблице могут быть одновременно связаны с несколькими записями в правой таблице.
Возьмём пример выше, где несколько книг могут иметь несколько писателей.
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 |