Ruby on Rails 3: Основы работы с ActiveRecord моделью

февраля 10, 2012  |  Published in Ruby on Rails, Ruby on Rails 3, Базы данных  |  4 Comments

ruby on rails tutorialRubyDev — ваш друг в изучении 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

Лучшая благодарность автору — ваши комментарии и популяризация ресурса! Спасибо всем за внимание к проекту и моральную поддержку его автора. Не забывайте о нашей группе во Вконтакте: !

Tags: , , , , , , ,

Responses

  1. Віталій says:

    февраля 10, 2012 at 23:41 (#)

    Какой практики лучше придерживаться когда нужно делать запросы с неопределенным количеством данных в запросе?
    Например, фильтр по товарам в интернет-магазине.

  2. admin says:

    февраля 12, 2012 at 03:13 (#)

    Виталий, лично я делаю в моделе метод в который скармливаю, например params[:query], а он конвертирует хэш запроса в формат, что подходит методу where. Иногда можно скормить сам запрос в where непосрественно, например если params[:query] содержит такой хэш: {name:»Some name»,price: 100..200}. Если не сильно сложные запросы то нужно стараться так организовать, чтобы сразу передавать в where.

  3. Sergey says:

    февраля 19, 2012 at 17:53 (#)

    Так же нужно отметить, что при обновлении update_attributes сначала выполняет валидацию и, если данные валидны, сохраняет. update_attribute же сохраняет данные в базу без проверки на валидность.

  4. Bighamster says:

    апреля 23, 2012 at 15:18 (#)

    Подскажите плиз способ, как можно получить sql запрос из объекта ActiveRecord не запуская его на выполнение? Мне казалось метод to_sql должен был бы это делать, но у меня в консоли User.all.to_sql падает с ошибкой (rails 3.2.1).

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.