Отказ от ответственности: то, что вы собираетесь прочесть, исходит от человека, который три дня назад начал изучать паттерн модель-представление-контроллер (MVC) и Ruby on Rails в рамках Launch School. Эта серия написана в основном как инструмент для меня, чтобы организовать свои мысли. Если другие также находят это полезным, то это делает его еще лучше. Наслаждаться.

Подготовка

В этом проекте я буду использовать Ruby 2.0.0 и Ruby on Rails 4.0.0. Почему? Launch School использовала его в то время. Если вы следуете этому примеру, убедитесь, что ruby -v выводит 2.0, а rails -v выводит 4.0. Если нет, иди за ними.

Начинать

Мы создадим простое приложение для управления учетными записями с использованием Ruby on Rails, в котором держатели могут иметь учетные записи и совершать транзакции. Я знаю, что это скучно, но это должно позволить нам пройтись по основам приложения rails, не углубляясь слишком далеко.

Чтобы запустить приложение Rails, перейдите в каталог, в котором вы хотите разместить основную папку проекта, и запустите rails new accountManager в терминале. Это создаст папку accountManager и многочисленные файлы, с большинством из которых мы не будем связываться. (С этого момента все команды терминала должны выполняться из основного каталога приложения.)

Исследуйте файлы, созданные Rails. Пять основных областей файлов проекта, которые нас будут интересовать, находятся в app/controllers, app/models, app/views, config/routes.rb, db/migrate. Обратите внимание, что db/migrate migrate не будет существовать до тех пор, пока не будет создана первая миграция. Все это будет со временем объяснено либо в этом посте, либо позже в этой серии.

Сервер можно запустить с помощью терминальной команды rails server и просмотреть в localhost:3000 в браузере. Rails должен отображать общую страницу «Добро пожаловать на борт» с несколькими советами о том, как начать работу. Мы не будем следовать этому в точности, так как будем использовать более ручной подход.

Настройка базы данных

Если вы используете SQLite в качестве базы данных, а это потому, что это база данных по умолчанию для Rails, то файл базы данных был создан при сборке приложения. Если у вас почему-то нет файла db/development.sqlite3, то запустите rake db:create в терминале.

Чтобы настроить схему базы данных, нам сначала нужен план. Это примет форму диаграммы отношений сущностей (ERD). Мы будем следовать этой дорожной карте для создания схемы и связей между таблицами.

Мы создадим каждую из этих таблиц и их ассоциации одну за другой. Таблицы названы с использованием соглашения Rails для таблиц базы данных. Для справки вот список ассоциаций, которые нам нужно сделать.

  1. У Transaction может быть один Account, а у Account может быть много Transaction. Это ассоциация «один ко многим».
  2. У Transaction может быть один Holder, а у Holder может быть много Transaction. Это ассоциация «один ко многим».
  3. У Holder может быть много Account, а у Account может быть много Holders. Account и Holder связаны через таблицу соединений holder_accounts. Это ассоциация «многие ко многим».

Приведенные выше имена в верхнем регистре станут классами моделей для каждого ресурса.

Запустите rails generate migration create_transactions, чтобы создать файл миграции, который будет использоваться для создания схемы для таблицы Transaction. Имя файла <creation_date>_create_transactions.db будет создано в папке db, где <creation_date> представляет время создания файла. Файл должен выглядеть так:

class CreateTransactions < ActiveRecord::Migration
  def change
    create_table :transactions do |t|
    end
  end
end

Rails пошли дальше и добавили метод create_table :transactions, потому что мы использовали create_transactions при генерации. Если этого кода нет в вашем, просто добавьте его. Теперь измените файл, включив в него столбцы таблицы, описанные в нашем ERD.

class CreateTransactions < ActiveRecord::Migration
  def change
    create_table :transactions do |t|
      t.string :kind
      t.integer :amount
      t.text :description
      t.timestamps
    end
  end
end

t.timestamps — это удобный метод, который указывает Rails создать столбцы created_at и updated_at в таблице. Rails будет автоматически поддерживать актуальность этих столбцов всякий раз, когда предпринимается действие. Теперь запустите rake db:migrate, чтобы применить миграцию и создать таблицу. Вы должны увидеть вывод, который говорит CreateTransactions: migrate.

(Сначала я пытался использовать type вместо kind в качестве имени столбца. Миграция прошла успешно, но позже я столкнулся с проблемами при попытке создать Transaction. Оказалось, что type — это ключевое слово, используемое в ActiveRecord. Помните об этом при создании имена.)

Настройка модели

Модель сделки

Теперь, когда у нас есть настройка базы данных для обработки транзакций, создайте модель Transaction, которая позволит нам работать с данными. Создайте файл с именем app/models/transaction.rb. Соглашение Rails состоит в том, чтобы назвать этот файл единственной версией таблицы в нижнем регистре.

class Transaction < ActiveRecord::Base
end

Просто создайте класс и наследуйте его от ActiveRecord::Base. Все классы моделей должны иметь это наследование. Это даст классу все полезные методы для доступа, обновления, создания и всесторонней работы с таблицей транзакций и ее ассоциациями.

На этом этапе мы должны убедиться, что Transaction правильно подключен к базе данных и что мы можем создавать и сохранять транзакции. Запрыгивайте в rails console (используя эту команду). Введите Transaction и просмотрите результат:

> Transaction
 => Transaction(id: integer, kind: string, amount: integer, description: text, created_at: datetime, updated_at: datetime, holder_id: integer, account_id: integer)

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

> transaction = Transaction.new(kind: "Checking", amount: 1500, description: "check")
 => #<Transaction id: nil, kind: "Checking", amount: 1500, description: "check", created_at: nil, updated_at: nil>
> transaction.save
   (0.1ms)  begin transaction
  SQL (1.4ms)  INSERT INTO "transactions" ("amount", "created_at", "kind", "updated_at") VALUES (?, ?, ?, ?, ?)  [["amount", 1500], ["created_at", Sat, 16 Dec 2017 00:21:05 UTC +00:00], ["description", "check"], ["kind", "Checking"], ["updated_at", Sat, 16 Dec 2017 00:21:05 UTC +00:00]]
   (4.7ms)  commit transaction
 => true

Несколько замечаний: Transaction#new не сохраняет транзакцию в базе данных; объект хранится только в памяти. Вот почему id, created_at и updated_at становятся nil при создании экземпляра. Также обратите внимание, что я не указал значения ни для одного из этих столбцов, Rails обработает их, когда транзакция будет сохранена в базе данных. Наконец, я проигнорировал столбцы holder_id и account_id, перечисленные в ERD. Мы добавим их при создании этих таблиц и ассоциаций.

Transaction.save обращается к базе данных и записывает данные в таблицу транзакций. Просмотрите SQL-запрос, созданный для обработки запроса. На данный момент транзакция была успешно сохранена, и наша база данных имеет одну строку в таблице транзакций. Вы можете перепроверить это с помощью Transaction.find(1) или Transaction.first. Оба должны вернуть только что созданную и сохраненную транзакцию.

Модель держателя и отношение «один ко многим»

Таким же образом создайте модель Holder. Я кратко перечислю шаги здесь:

  1. Запустите rails generate migration create_holders, чтобы создать миграцию.
  2. Отредактируйте файл миграции, чтобы указать Rails создать таблицу Holder с соответствующими столбцами. Не для отметок времени.
  3. Запустите rake db:migrate и создайте модель Holder. Убедитесь, что Holder правильно подключен в консоли.

Если все прошло хорошо, теперь у вас должен быть новый файл миграции и файл app/models/holder.rb, аналогичный транзакциям.

# db/yyyymmddhhmmss_create_holders.rb
class CreateHolders < ActiveRecord::Migration
  def change
    create_table :holders do |t|
      t.string :name
      t.timestamps
    end
  end
end
# app/models/holder.rb
class Holder < ActiveRecord::Base
end

Создание отношения «один ко многим». На этом этапе мы можем работать с транзакциями и держателями, но они никак не связаны. Rails выполняет это с помощью вспомогательных методов, называемых has_one, has_many, belongs_to и has_and_belongs_to_many. Поскольку у нас отношение «один ко многим», мы будем использовать belongs_to и has_many. Измените holder.rb и transaction.rb на следующие:

# holder.rb
class Holder < ActiveRecord::Base
  has_many :transactions
end
# transaction.rb
class Transaction < ActiveRecord::Base
  belongs_to :holder
end

Эти два простых дополнения образуют отношения. По соглашению Rails будет искать столбец holder_id в таблице transactions при поиске транзакций держателя. Если бы вместо этого мы хотели назвать владельца учетной записи как-то иначе, например «владелец», но у нас все еще есть таблица holders в базе данных, мы могли бы использовать вместо этого следующее:

# transaction.rb
class Transaction < ActiveRecord::Base
  belongs_to :owner, class_name: :holder, foreign_key: :holder_id
end

Мы можем сделать это в определенных ситуациях, чтобы сделать имена более гармонирующими с отношениями, но holder для нас достаточно прямо сейчас.

В настоящее время в таблице транзакций нет столбца holder_id. Нам нужно создать его с помощью другой миграции. Запустите rails generate migration add_holder_id_to_transactions и поместите это в полученный файл.

class AddHolderIdToTransactions < ActiveRecord::Migration
  def change
    add_column :transactions, :holder_id, :integer
  end
end

Запустите миграцию с помощью rake db:migrate. Убедитесь, что Transaction теперь имеет столбец holder_id в консоли с rails console, а затем Transaction.new.

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

  • transaction = Transaction.create(kind: "deposit", amount: 2000, description: "check") (create эффективно запускает new и save одновременно.)
  • transaction.holder (Это должно вернуть nil, так как в настоящее время нет держателя, связанного с этой транзакцией.)
  • holder = Holder.create(name: "Alex")
  • holder.transactions (Это должно вернуть пустую коллекцию, подобную массиву, поскольку с этим держателем не связаны транзакции.)
  • holder.transactions << transaction (присваивает holder_id транзакции и добавляет ее в коллекцию транзакций.)
  • Пройдите через ассоциации и исследуйте. holder.transactions.first.holder.name (должно вернуть имя первоначального владельца)
  • С другой стороны transaction.holder.name (должно вернуть имя первоначального держателя)

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

Модель учетной записи и отношение «один ко многим»

В нашем приложении у держателя может быть много транзакций, но эти транзакции должны происходить на счете. Я оставлю эту часть на ваше усмотрение. Используйте модель держателя в качестве примера для создания отношения «один ко многим», когда одна учетная запись может иметь много транзакций. В приведенных ниже решениях указаны файлы, которые были обновлены. Не забудьте запустить db:migrate после редактирования новых файлов миграции.

# db/yyyymmddhhmmss_create_accounts.rb
class CreateAccounts < ActiveRecord::Migration
  def change
    create_table :accounts do |t|
      t.string :name
      t.string :kind
      t.integer :balance, default: 0
      t.timestamps
    end
  end
end
# db/yyyymmddhhmmss_add_account_id_to_transactions.rb
class AddAccountIdToTransactions < ActiveRecord::Migration
  def change
    add_column :transactions, :account_id, :integer
  end
end
# app/models/account.rb
class Account < ActiveRecord::Base
  has_many :transactions
end
# app/models/transactions.rb
class Transaction < ActiveRecord::Base
  belongs_to :holder
  belongs_to :account
end

Примечание. Вы можете объединить две миграции в одну, если считаете это необходимым, поскольку на этом этапе вы знаете, что нам понадобится столбец account_id.

Предупреждение: всегда используйте миграцию для изменения базы данных. Изменение уже запущенных миграций (независимо от того, выполнялись ли они вами или членами вашей команды) опасно. Небольшой пример: если вы удалили столбец из старой миграции, но более поздняя миграция все еще пытается изменить этот столбец. Хотя выполнение чего-то подобного может работать на вашем компьютере, если бы кто-то другой взял ваш код и запустил миграцию, этот конкретный столбец никогда не был бы создан. Ошибка: вы не можете изменить несуществующий столбец.

Отношение «владелец-аккаунт» «многие ко многим»

Окончательное отношение, которое нам нужно установить, позволит владельцу иметь много счетов (как и многие люди), а счету — много держателей (например, совместный расчетный счет).

У нас есть две основные таблицы accounts и holders. Теперь нам нужно создать таблицу соединений, чтобы соединить их. Это делается аналогично другим таблицам, но ассоциации могут быть немного сложными.

Сгенерируйте миграцию, чтобы создать таблицу соединений. Имена для таблиц, которые содержат несколько слов, могут быть сложными, поэтому Rails предоставляет вспомогательный метод #tableize для генерации имени таблицы, которое Rails будет использовать для данной модели. В нашем случае мы хотим, чтобы класс (наша модель) был HolderAccount. Перейдите в консоль rails и запустите "HolderAccount".tableize, который должен вернуть "holder_accounts". Шаблон для этого обычно <first[singular]_second[plural]>, все в нижнем регистре. Выйдите из консоли и запустите rails generate migration create_holder_accounts. Сделайте так, чтобы ваш файл миграции выглядел так:

class CreateHolderAccounts < ActiveRecord::Migration
  def change
    create_table :holder_accounts do |t|
      t.integer :holder_id, :account_id
      t.timestamps
    end
  end
end

И мы могли бы также создать модель HolderAccounts во вновь созданном файле holder_account.rb. Обратите внимание, что принято использовать оба существительных в единственном числе с подчеркиванием.

class HolderAccount < ActiveRecord::Base
end

Запустите rake db:migrate, затем убедитесь, что HolderAccount правильно подключен в консоли.

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

  • Каждая строка holder_accounts связана с одним holder и одним account.
  • У каждого account и holder может быть много holder_accounts. Итак, вот наши отношения.
# holder.rb
class Holder < ActiveRecord::Base
  has_many :transactions
  has_many :holder_accounts
  has_many :accounts, through: :holder_accounts
end
# holder_account.rb
class HolderAccount < ActiveRecord::Base
  belongs_to :holder
  belongs_to :account
end
# account.rb
class Account < ActiveRecord::Base
  has_many :transactions
  has_many :holder_accounts
  has_many :holders, through: :holder_accounts
end

Дополнительная работа здесь, конечно, through: :holder_accounts. Это бит, который говорит связать holders и accounts через их идентификаторы в таблице holder_accounts.

Подтверждение ассоциации владельца и учетной записи — проверьте! Перед запуском этих команд убедитесь, что у вас есть хотя бы один держатель и одна учетная запись.

holder = Holder.first
holder.accounts # return an empty collection
account = Account.first
account.holders # return an empty collection

Если модели были подключены неправильно, будут выданы ошибки. Вы можете создать их с помощью Holder.create и Account.create. Теперь добавьте учетную запись держателя.

holder.accounts << account # assigns the account to the holder
holder.accounts.first.name 
# returns name of first account assigned to holder
account.holders.first.name
# returns name of first holder assigned to account

Теперь мы можем пройтись по всем нашим данным настолько запутанным образом, насколько нам нравится:

account.holders.first.transactions.first.account.name

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

Заворачивать

Мы увидели здесь, что приложение Ruby on Rails может быть создано с относительно небольшими усилиями. Эта структура заботится о многих деталях, предполагая множество соглашений. В частности, мы видели, что имена практически всего принимают определенную форму. Имена классов модели, имена файлов, имена первичных и внешних ключей в базах данных и многое другое должны соответствовать соглашениям Rails. Rails позволяет перезаписать это небольшим количеством кода, но в этом нет необходимости.

Теперь у нашего менеджера по работе с клиентами есть простая, но надежная база данных, с которой мы можем работать в будущем. Что касается шаблона MVC, эта запись касалась только «M». В следующий раз мы будем работать с представлениями и контроллерами, чтобы мы могли видеть наши данные в браузере.