Ruby on Rails 3: Основы работы с ActiveRecord моделью
февраля 10, 2012 | Published in Ruby on Rails, Ruby on Rails 3, Базы данных | 4 Comments
RubyDev — ваш друг в изучении Ruby и Rails!
Что такое модель?
Модель — это составляющая часть архитектурного паттерна MVC, которая хранит бизнес логику. В вашем приложении все модели должны храниться в директории app/models в отдельных файлах, имена которых должны соответствовать имени модели, например: Post -> post.rb, InvoiceProduct -> invoice_product.rb.
Как связаны модель и таблица?
В большинстве случаев модель ассоциируется с определенной таблицей в базе данных, например модель Post будет ассоциироваться с таблицей posts. Экземпляр модели являет собой объект представляющий одну запись из таблицы, а сама модель (класс) используется для работы со всей таблицей, например для поиска всех записей соответствующих некоторому условию.
Обратите внимание на то, как называется модель и как называется соответствующая ей таблица. Имя модели — всегда в едином числе, а имя таблицы во множественном.
Как создать модель?
Создать модель можно вручную, но лично я предпочитаю использовать генератор моделей:
$ rails g model Product name:string description:text price:integer
invoke active_record
create db/migrate/20120210115708_create_products.rb
create app/models/product.rb
invoke test_unit
create test/unit/product_test.rb
create test/fixtures/products.yml
Вызывая генератор я передаю в него в качестве аргументов имя модели и имена полей таблицы ассоциируемой с моделью вместе с типами данных этих полей (столбцов).
Генератор модели мне кажется более удобным потому, что одной командой я создаю сразу все необходимые для работы файлы: собственно модель, миграцию для создания соответствующей модели таблицы в БД, файл фикстур и болванку для написания тестов.
Что такое консоль Rails и как с ней работать?
Консоль Rails — это обычный IRB с автоматической загрузкой кода вашего приложения. Консоль Rails позволяет вручную изучать и испытывать приложение.
Для вызова консоли Rails необходимо выполнить следующую команду:
$ rails c
Loading development environment (Rails 3.1.1)
ruby-1.9.2-p180 :001 >
Далее в консоли вы можете писать Ruby код и работать с объектами вашего приложения.
ruby-1.9.2-p180 :001 > Product
=> Product(Table doesn’t exist)
«Table doesn’t exist» — означает, что таблица ассоциируемая с моделью отсутствует. Вспомните прошлую статью о миграциях. Мы забыли выполнить сгенерированную вместе с моделью миграцию для создания таблицы. Вам необходимо выполнить ее. Далее мы рассмотрим работу с консолью Rails более подробно.
Как включить логирование запросов к базе данных в консоли?
Для включения логирования в консоли необходимо ввести следующий код:
ActiveRecord::Base.logger = Logger.new(STDOUT)
Для того, чтобы постоянно не набирать этот код в консоли, вы можете добавить его в файл config/environments/development.rb. Вот что мы можем заметить используя логгер:
$ rails c
Loading development environment (Rails 3.1.1)
ruby-1.9.2-p180 :001 > Product
=> Product(id: integer, name: string, description: text, price: integer, created_at: datetime, updated_at: datetime)
ruby-1.9.2-p180 :002 > Product.all
Product Load (0.3ms) SELECT «products».* FROM «products»
=> []
Теперь вы видите SQL-запросы, которые приходят от вашего приложения к серверу баз данных и вам легче понимать как работает тот или иной метод и оптимизировать работу приложения.
Как создать новую запись?
Для создания новой записи в БД чаще всего используют метод new или create:
product = Product.new name:"Socks", description:"Red socks", price:10 #=> #<Product id: nil, name: "Socks", description: "Red socks", price: 10, created_at: nil, updated_at: nil> product.new_record? #=> true product.save #SQL (99.9ms) INSERT INTO "products" ("created_at", "description", "name", "price", "updated_at") VALUES (?, ?, ?, ?, ?) [["created_at", Fri, 10 Feb 2012 12:30:55 UTC +00:00], ["description", "Red socks"], ["name", "Socks"], ["price", 10], ["updated_at", Fri, 10 Feb 2012 12:30:55 UTC +00:00]] #=> true product.new_record? #=> false
Метод new создает новую экземпляр модели в памяти компьютера и заполняет ее поля переданными данным. Чтобы экземпляр модели превратился в запись в базе данных необходимо вызвать ля него метод save. Метод new_record? позволяет узнать сохранена ли запись в таблицу или хранится только в памяти.
Метод create отличается от метода new тем, что автоматически выполняет сохранение экземпляра модели в таблицу БД:
product = Product.create name:"Socks", description:"Green socks", price:12 #SQL (1.1ms) INSERT INTO "products" ("created_at", "description", "name", "price", "updated_at") VALUES (?, ?, ?, ?, ?) [["created_at", Fri, 10 Feb 2012 12:34:15 UTC +00:00], ["description", "Green socks"], ["name", "Socks"], ["price", 12], ["updated_at", Fri, 10 Feb 2012 12:34:15 UTC +00:00]] #=> #<Product id: 2, name: "Socks", description: "Green socks", price: 12, created_at: "2012-02-10 12:34:15", updated_at: "2012-02-10 12:34:15"> product.new_record? #=> false
Как сделать выборку записей из таблицы?
Для выборки записей из таблицы имеется несколько различных методов, ниже приведены некоторые из них.
all — выполняет выборку всех записей:
Product.all #Product Load (0.3ms) SELECT "products".* FROM "products" #=> [#<Product id: 1, name: "Socks", description: "Red socks", price: 10, created_at: "2012-02-10 12:30:55", updated_at: "2012-02-10 12:30:55">, #<Product id: 2, name: "Socks", description: "Green socks", price: 12, created_at: "2012-02-10 12:34:15", updated_at: "2012-02-10 12:34:15">]
first — возвращает первую запись:
Product.first #Product Load (0.2ms) SELECT "products".* FROM "products" LIMIT 1 #=> #<Product id: 1, name: "Socks", description: "Red socks", price: 10, created_at: "2012-02-10 12:30:55", updated_at: "2012-02-10 12:30:55">
last — возвращает последнюю запись:
Product.last #Product Load (0.3ms) SELECT "products".* FROM "products" ORDER BY "products"."id" DESC LIMIT 1 #=> #<Product id: 2, name: "Socks", description: "Green socks", price: 12, created_at: "2012-02-10 12:34:15", updated_at: "2012-02-10 12:34:15"> [ruby] find - возвращает запись с указанным идентификатором (id): [ruby] Product.find(1) #Product Load (0.3ms) SELECT "products".* FROM "products" WHERE "products"."id" = ? LIMIT 1 [["id", 1]] #=> #<Product id: 1, name: "Socks", description: "Red socks", price: 10, created_at: "2012-02-10 12:30:55", updated_at: "2012-02-10 12:30:55"> Product.find(1,2) #Product Load (0.3ms) SELECT "products".* FROM "products" WHERE "products"."id" IN (1, 2) #=> [#<Product id: 1, name: "Socks", description: "Red socks", price: 10, created_at: "2012-02-10 12:30:55", updated_at: "2012-02-10 12:30:55">, #<Product id: 2, name: "Socks", description: "Green socks", price: 12, created_at: "2012-02-10 12:34:15", updated_at: "2012-02-10 12:34:15">] Product.find((1..2).to_a) #Product Load (0.3ms) SELECT "products".* FROM "products" WHERE "products"."id" IN (1, 2) #=> [#<Product id: 1, name: "Socks", description: "Red socks", price: 10, created_at: "2012-02-10 12:30:55", updated_at: "2012-02-10 12:30:55">, #<Product id: 2, name: "Socks", description: "Green socks", price: 12, created_at: "2012-02-10 12:34:15", updated_at: "2012-02-10 12:34:15">]
find_by_…_and_… — выборка по значениям определенных атрибутов:
Product.find_by_id(2) #Product Load (0.3ms) SELECT "products".* FROM "products" WHERE "products"."id" = 2 LIMIT 1 #=> #<Product id: 2, name: "Socks", description: "Green socks", price: 12, created_at: "2012-02-10 12:34:15", updated_at: "2012-02-10 12:34:15"> Product.find_by_name("Socks") #Product Load (0.3ms) SELECT "products".* FROM "products" WHERE "products"."name" = 'Socks' LIMIT 1 #=> #<Product id: 1, name: "Socks", description: "Red socks", price: 10, created_at: "2012-02-10 12:30:55", updated_at: "2012-02-10 12:30:55"> Product.find_by_name_and_description("Socks","ololo") #Product Load (0.3ms) SELECT "products".* FROM "products" WHERE "products"."name" = 'Socks' AND "products"."description" = 'ololo' LIMIT 1 #=> nil Product.find_by_name_and_description("Socks","Green socks") #Product Load (0.3ms) SELECT "products".* FROM "products" WHERE "products"."name" = 'Socks' AND "products"."description" = 'Green socks' LIMIT 1 #=> #<Product id: 2, name: "Socks", description: "Green socks", price: 12, created_at: "2012-02-10 12:34:15", updated_at: "2012-02-10 12:34:15"> Product.find_by_id_and_name_and_description(2,"Socks","Green socks") #Product Load (0.3ms) SELECT "products".* FROM "products" WHERE "products"."id" = 2 AND "products"."name" = 'Socks' AND "products"."description" = 'Green socks' LIMIT 1 #=> #<Product id: 2, name: "Socks", description: "Green socks", price: 12, created_at: "2012-02-10 12:34:15", updated_at: "2012-02-10 12:34:15">
where — выборка по нескольким условиям:
Product.where id: 1 #Product Load (0.2ms) SELECT "products".* FROM "products" WHERE "products"."id" = 1 #=> [#<Product id: 1, name: "Socks", description: "Red socks", price: 10, created_at: "2012-02-10 12:30:55", updated_at: "2012-02-10 12:30:55">] Product.where id: 1, name:"Socks" #Product Load (0.3ms) SELECT "products".* FROM "products" WHERE "products"."id" = 1 AND "products"."name" = 'Socks' #=> [#<Product id: 1, name: "Socks", description: "Red socks", price: 10, created_at: "2012-02-10 12:30:55", updated_at: "2012-02-10 12:30:55">] Product.where description: ["Red socks","Green socks"] #Product Load (0.3ms) SELECT "products".* FROM "products" WHERE "products"."description" IN ('Red socks', 'Green socks') #=> [#<Product id: 1, name: "Socks", description: "Red socks", price: 10, created_at: "2012-02-10 12:30:55", updated_at: "2012-02-10 12:30:55">, #<Product id: 2, name: "Socks", description: "Green socks", price: 12, created_at: "2012-02-10 12:34:15", updated_at: "2012-02-10 12:34:15">]
Метод where также может принимать в качестве аргумента строку с SQL-условием:
Product.where "products.id = 1" #Product Load (0.2ms) SELECT "products".* FROM "products" WHERE (products.id = 1) #=> [#<Product id: 1, name: "Socks", description: "Red socks", price: 10, created_at: "2012-02-10 12:30:55", updated_at: "2012-02-10 12:30:55">] Product.where "products.id > 1" #Product Load (0.2ms) SELECT "products".* FROM "products" WHERE (products.id > 1) #=> [#<Product id: 2, name: "Socks", description: "Green socks", price: 12, created_at: "2012-02-10 12:34:15", updated_at: "2012-02-10 12:34:15">] Product.where "products.id != 1" #Product Load (0.2ms) SELECT "products".* FROM "products" WHERE (products.id != 1) #=> [#<Product id: 2, name: "Socks", description: "Green socks", price: 12, created_at: "2012-02-10 12:34:15", updated_at: "2012-02-10 12:34:15">] Product.where "products.id = 1 OR products.id = 2" #Product Load (0.3ms) SELECT "products".* FROM "products" WHERE (products.id = 1 OR products.id = 2) #=> [#<Product id: 1, name: "Socks", description: "Red socks", price: 10, created_at: "2012-02-10 12:30:55", updated_at: "2012-02-10 12:30:55">, #<Product id: 2, name: "Socks", description: "Green socks", price: 12, created_at: "2012-02-10 12:34:15", updated_at: "2012-02-10 12:34:15">] Product.where "products.id IN (1, 2)" #Product Load (0.3ms) SELECT "products".* FROM "products" WHERE (products.id IN (1, 2)) #=> [#<Product id: 1, name: "Socks", description: "Red socks", price: 10, created_at: "2012-02-10 12:30:55", updated_at: "2012-02-10 12:30:55">, #<Product id: 2, name: "Socks", description: "Green socks", price: 12, created_at: "2012-02-10 12:34:15", updated_at: "2012-02-10 12:34:15">]
Совместно с методом where могут быть использованы методы — «ограничители», которые приведены ниже.
limit — данный метод добавляет LIMIT в SQL запрос чем ограничивает количество записей.
Product.where("products.id IN (1,2,3)").limit(1) #Product Load (0.3ms) SELECT "products".* FROM "products" WHERE (products.id IN (1,2,3)) LIMIT 1 #=> [#<Product id: 1, name: "Socks", description: "Red socks", price: 10, created_at: "2012-02-10 12:30:55", updated_at: "2012-02-10 12:30:55">]
offset — метод добавляет OFFSET в SQL условием чем позволяет указать id записи с которой будет начат поиск (все предыдущие записи игнорируются). offset часто используется совместно с limit для реализации пагинации.
Product.where("products.id IN (1,2,3)").offset(1) #Product Load (0.3ms) SELECT "products".* FROM "products" WHERE (products.id IN (1,2,3)) LIMIT -1 OFFSET 1 #=> [#<Product id: 2, name: "Socks", description: "Green socks", price: 12, created_at: "2012-02-10 12:34:15", updated_at: "2012-02-10 12:34:15">]
select — этот метод используется для оптимизации запроса путем указания того, какие именно данные из таблицы необходимо взять. Предположим, что вы хотите на главной странице сайта разместить ссылки на 20 последних постов в блоге компании. Для того, чтобы создать такие ссылки вам нужен только индексы и заголовки записей, но не записи целиком. Для этой цели и используется метод select — он позволяет загружать в память только реально необходимые данные.
p = Product.where(id: 1).select([:id, :name]).first #Product Load (0.2ms) SELECT id, name FROM "products" WHERE "products"."id" = 1 LIMIT 1 #=> #<Product id: 1, name: "Socks"> p.name #=> "Socks" p.id #=> 1 p.description #ActiveModel::MissingAttributeError: missing attribute: description
Как изменить запись?
Для изменения записи лучше всего подходят методы update_attribute и update_attributes.
update_attribute(<имя атрибута(поля,столбца)>,<новое значение>) используется для обновления определенного атрибута записи.
update_attributes(<хэш со значением атрибутов>) — используется для полного или частичного обновления атрибутов записи.
product = Product.last #Product Load (0.2ms) SELECT "products".* FROM "products" ORDER BY "products"."id" DESC LIMIT 1 #=> #<Product id: 2, name: "Socks", description: "Green socks", price: 12, created_at: "2012-02-10 12:34:15", updated_at: "2012-02-10 12:34:15"> product.update_attribute(:price, 13) #(65.3ms) UPDATE "products" SET "price" = 13, "updated_at" = '2012-02-10 18:37:26.369679' WHERE "products"."id" = 2 #=> true product.update_attributes(description:"Blue socks", price:15) #(0.3ms) UPDATE "products" SET "description" = 'Blue socks', "price" = 15, "updated_at" = '2012-02-10 18:38:23.118707' WHERE "products"."id" = 2 #=> true
Кроме описанных выше способов можно также использовать методы-аксессоры атрибутов, но его стараются избегать.
product.description #=> "Blue socks" product.description = "Yellow socks" #=> "Yellow socks" product.save #(0.3ms) UPDATE "products" SET "description" = 'Yellow socks', "updated_at" = '2012-02-10 18:41:40.966205' WHERE "products"."id" = 2 #=> true
Как удалить запись?
Для удаления записи чаще всего используют метод destroy.
product.destroy #SQL (0.3ms) DELETE FROM "products" WHERE "products"."id" = ? [["id", 2]] #=> #<Product id: 2, name: "Socks", description: "Yellow socks", price: 15, created_at: "2012-02-10 12:34:15", updated_at: "2012-02-10 18:41:40"> Product.destroy(13) #Product Load (0.2ms) SELECT "products".* FROM "products" WHERE "products"."id" = ? LIMIT 1 [["id", 13]] #SQL (0.2ms) DELETE FROM "products" WHERE "products"."id" = ? [["id", 13]] #=> #<Product id: 13, name: "Socks", description: "Orange socks", price: 12, created_at: "2012-02-10 18:56:41", updated_at: "2012-02-10 18:56:41">
Вы также можете использовать метод destroy_all, что принадлежит классу для уничтожения всех записей или некоторого их набора:
Product.where(id: 1).destroy_all #Product Load (0.2ms) SELECT "products".* FROM "products" WHERE "products"."id" = 1 #SQL (0.2ms) DELETE FROM "products" WHERE "products"."id" = ? [["id", 1]] #=> [#<Product id: 1, name: "Socks", description: "Red socks", price: 10, created_at: "2012-02-10 12:30:55", updated_at: "2012-02-10 12:30:55">]
Помимо методов destroy и destroy_all существуют методы delete и delete_all. Они выполняют абсолютно ту же работу, но быстрее за счет того, что для них нет коллбеков (выполнения кода для обработки событий before_destroy, after_destroy), то есть запись удаляется «грубо» из-за чего эти методы применяют редко.
Product.first.delete #Product Load (0.3ms) SELECT "products".* FROM "products" LIMIT 1 #SQL (116.8ms) DELETE FROM "products" WHERE "products"."id" = 9 #=> #<Product id: 9, name: "Socks", description: "Orange socks", price: 12, created_at: "2012-02-10 18:49:54", updated_at: "2012-02-10 18:49:54"> Product.delete(12) #SQL (85.1ms) DELETE FROM "products" WHERE "products"."id" = 12 #=> 1 Product.delete_all #SQL (108.1ms) DELETE FROM "products" #=> 2 #Product.where(name: "Socks").delete_all #SQL (92.1ms) DELETE FROM "products" WHERE "products"."name" = 'Socks' #=> 2
Лучшая благодарность автору — ваши комментарии и популяризация ресурса! Спасибо всем за внимание к проекту и моральную поддержку его автора. Не забывайте о нашей группе во Вконтакте: !
февраля 10, 2012 at 23:41 (#)
Какой практики лучше придерживаться когда нужно делать запросы с неопределенным количеством данных в запросе?
Например, фильтр по товарам в интернет-магазине.
февраля 12, 2012 at 03:13 (#)
Виталий, лично я делаю в моделе метод в который скармливаю, например params[:query], а он конвертирует хэш запроса в формат, что подходит методу where. Иногда можно скормить сам запрос в where непосрественно, например если params[:query] содержит такой хэш: {name:»Some name»,price: 100..200}. Если не сильно сложные запросы то нужно стараться так организовать, чтобы сразу передавать в where.
февраля 19, 2012 at 17:53 (#)
Так же нужно отметить, что при обновлении update_attributes сначала выполняет валидацию и, если данные валидны, сохраняет. update_attribute же сохраняет данные в базу без проверки на валидность.
апреля 23, 2012 at 15:18 (#)
Подскажите плиз способ, как можно получить sql запрос из объекта ActiveRecord не запуская его на выполнение? Мне казалось метод to_sql должен был бы это делать, но у меня в консоли User.all.to_sql падает с ошибкой (rails 3.2.1).