Интерфейс запросов ActiveRecord
Перевод: Active Record Query Interface
Это руководство описывает различные способы получать данные от базы данных, используя ActiveRecord. Прочтя это руководство, Вы сможете:
- Искать записи в базе данных с помощью множества методов и всевозможных условий
- Определять сортировку, требуемые атрибуты, группировку и другие свойства для поиска записей
- Использовать отложенную загрузку, чтобы сократить количество запросов к базе данных, необходимых для поиска
- Использовать динамические методы поиска
- Создавать именованные области видимости (named_scope), для добавления новых средств поиска Ваших моделей
- Проверять существование определенных записей
- Выполнять различные расчеты в моделях ActiveRecord
Если Вы привыкли использовать чистый SQL для поиска в базе, то обнаружите что в Rails есть более удобные способы делать тоже самое. В большинстве случаев ActiveRecord избавит Вас от необходимости писать SQL код.
Ниже определены модели, которые используют примеры этого руководства:
Все эти модели использует id в качестве первичного ключа, если, конечно, не оговорено другое
has_one :address
has_one :mailing_address
has_many :orders
has_and_belongs_to_many :roles
end
class Address < ActiveRecord::Base
belongs_to :client
end
class MailingAddress < Address
end
class Order < ActiveRecord::Base
belongs_to :client, :counter_cache => true
end
class Role < ActiveRecord::Base
has_and_belongs_to_many :clients
end
ActiveRecord совместима со множеством баз данных (к примеру: MySQL, PostgreSQL и SQLite). Независимо от используемых баз данных, ActiveRecord предоставляет унифицированный доступ к ним.
Поиск объектов в базе данных
Для поиск объектов в базе данных, ActiveRecord предоставляет метод класса Model.find. Этот метод позволяет осуществлять поиск в баз,е используя разные параметры, без необходимости использовать чистый SQL.
Так что же делает метод Model.find(options)?
- Преобразует переданные параметры в SQL запрос.
- Выполняет SQL запрос и получает требуемые строки из базы данных.
- Создает эквивалентный Ruby объект, соответствующей модели данных для каждой найденной строки.
- Выполняет обратные вызовы after_find если таковые имеются.
Поиск единственного объекта
Active Record позволяет искать единственный объект тремя различными способами.
Используя первичный ключ
Используя Model.find(primary_key, options = nil), Вы ищите объект у которого первичный ключ равен primary_key. К примеру:
client = Client.find(10)
=> #<Client id: 10, name: => "Ryan">
Этот код выполнит такой SQL запрос:
Если нет ни одной записи соответствующей условию, то метод Model.find(primary_key) вызывает исключение ActiveRecord::RecordNotFound.
Используя метод first
Model.first(options = nil) ищет первую строку, соответствующую условию отпределенному в options. Если options неопределен, то возвращается первая запись. К примеру:
=> #<Client id: 1, name: => "Lifo">
SQL запрос:
Model.first возвращает nil если ни одной записи не найлено. Исключение не вызывается.
Model.find(:first, options) равноценен Model.first(options)
PS: Второй вариант устарел
Используя метод last
Метод Model.last(options = nil) возвращает последнюю запись соответстующую условию отпределенному в options. Если options неопределена, то возвращается последняя запись. К примеру:
=> #<Client id: 221, name: => "Russel">
SQL запрос:
Model.last возвращает nil если ни одной записи не найлено. Исключение не вызывается.
Model.find(:last, options) равноценен Model.last(options)
PS: Второй вариант устарел
Поиск множетва объектов
Используя масив первичных ключей
Model.find(array_of_primary_key, options = nil) так же принимает массив с первичными ключами. Будет возвращен массив с соответствующими записями. К примеру:
client = Client.find(1, 10) # Или так Client.find([1, 10])
=> [#<Client id: 1, name: => "Lifo">, #<Client id: 10, name: => "Ryan">]
SQL запрос:
Если хоть одна запись не найдена, то метод Model.find(array_of_primary_key) вызовет исключение ActiveRecord::RecordNotFound.
Используя find :all
Метод Model.all(options = nil) находит все записи соответствующие условию, описанному в options. Если options не указан, то возвращаются все записи из таблицы.
clients = Client.all
=> [#<Client id: 1, name: => "Lifo">, #<Client id: 10, name: => "Ryan">, #<Client id: 221, name: => "Russel">]
SQL запрос:
Метод Model.all возвращает пустой масив, если нет совпадений. Исключение не вызывается.
Model.find(:all, options) соответствует Model.all(options)
PS: Второй вариант устарел
Поиск множества объектов в пакетной обработке
Иногда нужно выполнить итерации по большому набору записей в базе данных. Например разослать спам по базе пользователей
, экспортировать данные, и т.д.
Первое что приходит в голову:
User.all.each do |user|
NewsLetter.weekly_deliver(user)
end
Если в базе слишком много данных, то этот подход слишком накладный. Это потому что метод User.all заставляет ActiveRecord выбрать всю таблицу и держать ее в памяти. Для большого количества объектов может потребоваться слишком много памяти.
Используя find_each
Для эффективной итерации по таблице с большим количеством записей, ActiveRecord предоставляет метод find_each
NewsLetter.weekly_deliver(user)
end
Конфигурирование размера выборки
Поумолчанию find_each последовательно выбирает по 1000 строк из базы данных и выполняет код в блоке, и так до тех пор пока не обойдет всю таблицу. Это значение можно переопределить с помощью параметра :batch_size.
Выбрать пользователей по 5000 штук:
NewsLetter.weekly_deliver(user)
end
Начало выборки с определенного первичного ключа
Записи выбираются в порядке возрастания первичного ключа, который должен быть целым числом. Опция :start позволяет изменить первичный ключ с которого начнется выборка. Это может быть полезно когда, например, мы хотим возобновить возобновить прерванную пакетную обработку, правда для этого нужно сохранять последний обработанный идентификатор.
Начать пакетную обработку пользователей, начиная с пользователя, с первичным ключом равным 2000:
NewsLetter.weekly_deliver(user)
end
Дополнительные параметры
Метод find_each принимает теже параметры что и обычный метод find. Однако :order и :limit используются методом и следовательно не могут быть переданны явно.
Используя метод find_in_batches
Вы так же можете работать с массивом записей, а не отдельной записью, используя метод find_in_batches. Этот метод действует так же как и find_each, но в блок передает массив объектов.
Invoice.find_in_batches(:include => :invoice_lines) do |invoices|
export.add_invoices(invoices)
end
Это передаст в блок массив счетов по 1000 за раз.
Условия
Метод find позволяет Вам определять условия для выборки записей, представляя раздел в WHERE оператора SELECT эти условия. Условия могут быть определены как строка, массив, или хэш.
Условия как строка
Если бы Вы хотели добавить условия к запросу, Вы могли бы только определить ключ :conditions, так как в примере: Client.first (:conditions => "orders_count = '2'"). Это найдет всех клиентов, у которых значение поля orders_count равно 2.
Но такой подход череват проблемами с безопасностью приложения, так как открывает лазейку для SQL инъекций. Например, Client.first(:conditions => "name LIKE '%#{params[:name]}%'") не безопасен. Как этого избежать? Смотри следующий раздел для безопасного способа определять условия, используя массив.
Условия как масив
Если значение orders_count должно меняться, просто передайте его как параметр. Запрос теперь будет выглядеть так:
ActiveRecord оставит перый элемент в условии и заменит в нем все вопросительные знаки (?) на на последующие элементы массива. (То есть размер массива должен быть на 1 элемент больше, чем число вопросительных знаков в условии)
Или если Вы хотите определить два условия, Вы можете сделать это как:
В этом примере первый вопросительный знак будет заменен значением params[:orders], а второй будет замененен SQL вариантом false, который зависит от адаптера.
Причина почему надо делать так:
а не так:
состоит в безопасной передаче параметров. Если мы разместим переменную непосредтвенно в строке SQL, то она будет передана базе данных без изменений. Это означает, что в запрос попадут непроверенные данные переданные пользователем, а они могут содержать зловредный код. В таком случае Вы создадите потенциальную угрозу для своей базы данных, потому что, как только пользователь узнает что может использовать Вашу базу в обход логики приложения, он обязательно этим воспользуется. Никогда не помещайте данные переданные пользователем непосредственно в строку запроса.
Для получения дополнительной информации о SQL injection, читайте Ruby on Rails Security Guide.
Именованные параметры
Вместо знаков вопроса можно использовать именованные параметры, а в массив условий подствалять хэш с парами ключ/значение:
Это удобнее особенно если у Вас большое число параметров
Условия диапазона
Если Вы ищете диапазон значений в таблице (например, пользователей созданных в определенном промежудке), Вы можете использовать подставить ruby-диапазон в оператор IN. Если в контроллер передали две даты, Вы бы так могли бы найти записи созданные в этом промежутке:
ActiveRecord сгенерировала бы запрос который является допустимым для небольших диапазонов, для больших диапазонов он не так хорош. Например, если Вы хотите получить данные за год, в котором 365 дней (или 366, в зависимости от года).
'2008-01-06','2008-01-07','2008-01-08','2008-01-09','2008-01-10','2008-01-11',
'2008-01-12','2008-01-13','2008-01-14','2008-01-15','2008-01-16','2008-01-17',
'2008-01-18','2008-01-19','2008-01-20','2008-01-21','2008-01-22','2008-01-23',
...
'2008-12-15','2008-12-16','2008-12-17','2008-12-18','2008-12-19','2008-12-20',
'2008-12-21','2008-12-22','2008-12-23','2008-12-24','2008-12-25','2008-12-26',
'2008-12-27','2008-12-28','2008-12-29','2008-12-30','2008-12-31'))
Условия для даты и времени
Действительно ужасные последствия могут быть, когда Вы используете диапазон Time объектов в условии, поскольку ActiveRecord попытается сравнить поле с каждой секундой в диапазоне:
Это может стать причиной появления ошибки, например MySQL возвратит ошибку: Got a packet bigger than 'max_allowed_packet' bytes: _query_
В этом примере было бы лучше использовать операторы > и <
Вы можете так же использовать операторы >= и <=
Точно также как в Ruby. Если Вы хотите использовать краткий синтаксис, прочитайте раздел “Условия как хэш” этого руководства.
Условия как хэш
ActiveRecord позволяет использовать в условии хэш, который повышает читаемость кода. Используя хэш Вы указываете в ключе поле таблицы, а в значении – искомое выражение.
ВЫ можете использовать только проверки на равенство, на наличие в диапазоне и на массив значений
Проверка на равенство
В качестве названия поля можно использовать и строку
Проверка на наличие в диапазоне
Хорошо то, что мы можем использовать диапазон без генерации большого запроса как в предидущем разделе.
Этот запрос находит всех клиентов созданных вчера используя BETWEEN оператор.
Это демонстрирует более короткий синтаксис для примеров в разделе “Условия как массив”.
Проверка на массив значений
Если Вы хотите найти записи, используя выражение IN, Вы можете передать массив в хэш условий:
Будет сгенерирован такой SQL запрос:
Параметры find
Кроме :conditions, Model.find принимает множество других параметров для того, чтобы настроить результат.
Model.find(:last, options_hash)
Model.find(:first, options_hash)
Model.first(options_hash)
Model.last(options_hash)
Model.all(options_hash)
Последующие разделы дают краткий обзор всех возможных параметров для options_hash.
Сортировка
Чтобы получить данные в определенном порядке, Вы должны определить параметр :orderпри вызове метода find.
Например, если Вы хотите получть список клиентов отсортированный по полю created_at, от должны сделать так:
Вы можете так же добавить ASC и DESC
Или сортировку по множественным полям:
Выборка определенных полей таблицы
По умолчанию, Model.find возвращает все поля таблицы, используя SELECT *.
Чтобы выбрать определенные поля таблицы, Вы можете определить их в параметре :select.
Если параметр :select использоваться, то все объекты будут доступны только для чтения.
Например, чтобы выбрать колонки только viewable_by и locked, используйте:
SQL запрос будет выглядеть так:
Будьте внимательны, потому что это также означает, что Вы инициализируете объект модели с только с теми аттрибутами, какие Вы выбрали. Если Вы попытаетесь обратиться к аттрибуту, который небыл инициализирован, то получите ошибку: ActiveRecord::MissingAttributeError: missing attribute: <attribute>
Попытка обратиться к аттрибуту id не вызовет исключения ActiveRecord:: MissingAttributeError, так что будьте внимательны, работая с ассоциациями, потому что для своей работы они нуждаются в аттрибуте id.
Параметре :select можно использовать функции SQL. Например, если Вам нужно получить записи из таблицы с уникальными значениями определенного поля используя функцию DISTINCT:
Параметры :limit и :offset
Чтобы использовать SQL конструкцию LIMIT, Вы должны передать Model.find параметры :limit и :offset.
:limit – максимальное число записей, которые вернет метод, а :offset – это начиная с какой записи будут возвращены данные. Например:
Этот код возратит максимум пять клиентов и, так как не определено смещение, это будут первые пять клиентов в таблице. Выпонится такой SQL запрос:
Если определены оба параметра: и :limit, и :offset:
Код вернет максимум пять клиентов и так как определено смещение, то они будут начинаться с пятого клиента в таблице. SQL запрос выглядет так:
Групировка
For example, if you want to find a collection of the dates orders were created on:
Чтобы использовать конструкцию GROUP BY в SQL, генерируемому методом Model.find, Вы должны определить параметр :group.
Например:
Это вернет по одному объекту Order для каждой даты, в которой есть заказы.
Получается такой SQL запрос:
Параметр :having
Чтобы использовать конструкцию HAVING в SQL, которая определяет условия для групировки GROUP BY, генерируемому методом Model.find, Вы должны определить параметр :having.
Например:
Вызов этого метода сгенерирует запрос:
Метод возвратит по одному заказу за каждый день прошлого месяца
Объекты только для чтения
Чтобы запретить модификацию/удвление отчетов, возвращенных Model.find, Вы должны определить параметр :readonly как true.
Любая попытка изменить или удалить объект будет вызывать исключение ActiveRecord::ReadOnlyRecord. К примеру так:
Следующий код вызовет исключение ActiveRecord::ReadOnlyRecord:
client.locked = false
client.save
Блокировка объектов для модификаций
Блокировка нужна для предотвращения одновременного изменения строки таблицы разными процессами, гарантируя атомарность модификаций. ActiveRecord обеспечивает два механизма блокировки:
- Оптимистическая блокировка
- Пессимистическая блокировка
Оптимистическая блокировка
Оптимистическая блокировка позволяет разным пользователям обращаться к одной и той же записи в таблици для редактирования, без возникновления конфликтов данных. Перед сохранением происходит проверка была ли изменена запись другим процессом после загрузки. Если изменение было то вызывается исключение ActiveRecord:: StaleObjectError и сохранении записи не происходит.
Столбец оптимистической блокировки
Чтобы использовать оптимистическую блокировку создайте в таблице столбец lock_version. Каждый раз, когда запись обновляется, ActiveRecord увеличивает на один значение в столбеце lock_version, если значение lock_version изенилось значит другой процесс модифицирова данные, значит вызывается исключение ActiveRecord:: StaleObjectError. Пример:
c2 = Client.find(1)
c1.name = "Michael"
c1.save
c2.name = "should fail"
c2.save # Raises a ActiveRecord::StaleObjectError
Если возникает конфликт данных Вы должны перехватить исключение и откатить назад изменения или как-то иначе решить конфликт. Вы так же должны быть уверены в том что колонка lock_version имеет значение по-умолчанию равное 0
Это поведение по-умолчанию, его можно отменить использя настройку ActiveRecord::Base.lock_optimistically = false.
Для смены названия блокировочной колонки ActiveRecord::Base имеет метод set_locking_column:
set_locking_column :lock_client_column
end
Пессимистическая блокировка
Пессимистическая блокировка использует механизм поддерживаемый базой данных. Передайте :lock => true в метод Model.find для включения эксклюзивной блокировки на выбранные строки. Model.find с :lock обычно выполняется внутри трансакций, для отката назад вслучае конфликта.
К примеру:
i = Item.first(:lock => true)
i.name = 'Jones'
i.save
end
Этот код сгенерирует такой SQL код для mySQL:
Item LOAD (0.3ms) SELECT * FROM `items` LIMIT 1 FOR UPDATE
Item UPDATE (0.4ms) UPDATE `items` SET `updated_at` = '2009-02-07 18:05:56', `name` = 'Jones' WHERE `id` = 1
SQL (0.8ms) COMMIT
В параметр :lock можно передать чистый SQL код, чтобы использовать различные типы блокировок. Например в mySQL есть выражение LOCK IN SHARE MODE для блокировки строки с возможностью другим запросам читать данные. Чтобы использовать эту блокировку, передайте это выражение в параметре :lock:
i = Item.find(1, :lock => "LOCK IN SHARE MODE")
i.increment!(:views)
end
Соединение таблиц
Метод Model.find имеет параметр :joins для соединения таблиц с помощью SQL выражения JOIN. Существует множество различных способов определить параметр :joins:
Использование фрагмента SQL
Вы можете передать параметром :joins чистый SQL код, он будет подставлен в запрос как JOIN выражение. Например:
создаст вот такой SQL запрос:
Использование масива/хэша для именованной ассоциации
Этот способ работает только как INNER JOIN.
ActiveRecord позволяет использовать имена ассоциаций, определенных в модели, как ссылку для параметра :joins.
К примеру создадим модели Category, Post, Comments и Guest:
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :category
has_many :comments
has_many :tags
end
class Comments < ActiveRecord::Base
belongs_to :post
has_one :guest
end
class Guest < ActiveRecord::Base
belongs_to :comment
end
Все последующие вызовы будут связывать таблицы используя выражение INNER JOIN:
Присоединение одной ассоциации
создаст SQL запрос:
Присоединение нескольких ассоциаций
создаст SQL запрос:
posts.*
FROM
posts
INNER JOIN categories ON posts.category_id = categories.id
INNER JOIN comments ON comments.post_id = posts.id
Присоединение вложенных ассоциаций (Один уровень вложенности)
Присоединение вложенных ассоциаций (Multiple Level)
Определение условий для присоединенных ассоциаций
Вы можете указать определенные условия для присоединенных таблиц, используя обычный массив или строку в параметре :conditions. При передачи хэша требуется специальный синтаксис для определения того к каким таблицам относится условие:
Client.all :joins => :orders, :conditions => {'orders.created_at' => time_range}
Другой более правильный синтаксис использует вложенный хэш:
Client.all :joins => :orders, :conditions => {:orders => {:created_at => time_range}}
Этот код найдет всех клиентов имеющих заказы созданные вчера, используя SQL выражение BETWEEN.
Немедленная загрузка ассоциаций
Немедленная загрузка – это механизм загрузки связанных объектов, возвращаемых методом Model.find, используя несколько запросов.
Проблема N + 1
Рассмотрим следующий код, который находит 10 клиентов и печатает и почтовый индекс:
clients.each do |client|
puts client.address.postcode
end
С первого взгляда выглядит неплохо. Но проблема в количестве запросов к базе данных. Код выполняет один запрос чтобы найти 10 клиентов + 10 запрсов (один на каждого найденного клиента) чтобы узнать почтовый индекс. Итого 11 запросов к базе. И чем больше значение параметра :limit, тем будет больше запросов.
Решение проблемы N + 1
ActiveRecord позволяет указывать ассоциации, которые должны быть загружены. Для этого необходимо указать параметр :include и передать в нем нужные ассоциации. ActiveRecord загрузит все указанные ассоциации используя минимальное количество запросов.
Если вернуться к предыдущему случаю, то код нужно переписать используя немедленную загрузку ассоциации :address:
clients.each do |client|
puts client.address.postcode
end
Этот код выполнит 2 запроса к базе, а не 11 как в предыдущем случае:
SELECT addresses.* FROM addresses WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))
Немедленная загрузка нескольких ассоциаций
ActiveRecord позволяет указывать любое количество ассоциаций в одном вызове метода Model.find, просто передайте в параметр :include массив или хэш, так же можно передать вложенный хэш, для вложенных ассоциаций.
Использование массива
Загрузятся все публикации и связанные объекты (категории и комментарии) для каждой публикации.
Хэш для вложенных связей
Этот код найдет категорию с id равным 1, также загрузит публикации из этой категории. Дополнительно загрузятся тэги и комментарии для каждой публикации. Для комментариев будут загружены ассоциации :guest.
Определение условий для немедленной загрузки
Несмотря на то, что ActiveRecord позволяет определять условия выборки для зспросов с немедленной загрузкой, так же как с :joins, все же рекомендуется вместо этого использовать :joins.
Динамические средства поиска
Для каждого поля (или атрибута) определенного в вашей таблице ActiveRecord поддерживает поисковый метод. Эсли вы имеете атрибут name в моделе Client, ActiveRecord добавляет методы find_by_name, find_all_by_name. Если Вы имеете атрибут locked в тойже модели, ActiveRecord добавит методы find_by_locked, find_all_by_locked.
Так же поддерживаются методы find_last_by_*, которые возвращают последнюю запись удовлетворяющую условию.
Чтобы заставить методы динамического поиска вызывать исключение ActiveRecord::RecordNotFound в случае, если записей не найдено, просто добавьте к ним восклицательный знак. Например, Client.find_by_name!("Ryan")
Если вы хотите искать по двум атрибутам, по name и по locked, просто соедините оба атрибута поставив между ними and, вот так: Client.find_by_name_and_locked("Ryan", true).
Есть еще один набор методов динамического поиска, которые ищут запись и если ничего не найдут, то создают запись. Их работа похожа на другие методы, использовать их можно так: find_or_create_by_name(params[:name]). Сначало происходит поиск записи в базе данных, потом ее создание, если запись не найдена. Метод Client.find_or_create_by_name("Ryan") выполнит следующие запросы:
BEGIN
INSERT INTO clients (name, updated_at, created_at, orders_count, locked) VALUES('Ryan', '2008-09-28 15:39:12', '2008-09-28 15:39:12', 0, '0')
COMMIT
Метод find_or_initialize похож на метод find_or_create, только для создания объекта использкет new, а не create. Например:
код либо назначает переменной client существующий объект с именем “Ryan” или инициализирует новый объект используя Client.new(:name => 'Ryan'). Далее Вы можете модифицировать любые его атрибуты, к примеру: client.locked = true и сохранить его в базе данных, используя save.
Поиск с использованием чистого SQL
Если Вы хотите использовать свой собственный SQL запрос, для поиска в базе, Вы можете использовать find_by_sql. Метод find_by_sql всегда возвращает массив объектов, даже SQL запрос возвращает одну записть. Пример:
find_by_sql обеспечивает простой способ выполнять свои собственные, не стандартные SQL запросы и получать набор инициализированных объектов.
select_all
Метод find_by_sql вызывает метод connection#select_all. Метод select_all возвращает массив хэш объектов с именами колонок в качестве ключей.
Проверка на существование объектов
Если Вы просто хотите проверить существование объекта, то для этого есть метод, exists?. Этот метод выполнит тот же самый SQL запрос что и метод find, но вместо того, чтобы возвратить объект или коллекцию объектов, он возвратит или true или false.
Метод exists? так же принимает массив идентификаторов ивозвртит true, если хоть один из этих объектов существует.
Более того, он принимает параметр :conditions, как метод find:
Его можно вызвать даже вообще без параметров:
Он вернет false если таблица пуста и true если нет.
Извлечение статистических данных
Этот раздел использует метод count для описания параметров методов описанных в подразделах.
Метод count принимает параметр :conditions, так же как метод exists?:
SQL запрос:
Вы можете использовать параметры :include и :joins чтобы сделать что-нибудь более сложное:
SQL код:
count
Если Вы хотите узнать, сколько записей находится в таблице, Вы можете вызвать Client.count. Чтобы найти определенных клиентов, нпример тех которых определен возраст можно использовать Client.count (:age).
Параметры функции описаны выше.
average
Если хотите вычислить среднее значение какого-нибудь поля, вызовите метод average. Примерно так:
Результатом будет число, являющиеся средним значением всех значений поля orders_count
Параметры функции описаны выше.
minimum
If you want to find the minimum value of a field in your table you can call the minimum method on the class that relates to the table. This method call will look something like this:
Если хотите узнать минимальное значение какого-нибудь столбца таблицы, вызывайте метод minimum
Параметры функции описаны выше.
maximum
Для максимального значения подойдет функция maximum:
Параметры функции описаны выше.
sum
Чтобы подсчитать сумму:
Параметры функции описаны выше.
SQL функции зависимые от типа базы данных
Все выше описанные методы работают в режиме, независимом от типа базы данных. Чтобы использовать специфичные функции воспользуйтесь методом calculate
Перевод: Андрей Лепешкин

Спасибо за переводы!
Антон
24 Окт 09 at 1:57
Спасибо. Очень приятно, что кому-то это пригодилось.
undr
25 Окт 09 at 10:40
Хороший сайт, особенно переводы доков по рельсам! Большое спасибо! Судя по всему я второй подписавшийся на блог буду
Alex
26 Окт 09 at 0:07