Если вы читаете эту статью, то, вероятно, уже знакомы с основами, касающимися таблиц SQL и того, как Active Record автоматически сопоставляет классы ruby с таблицами sql. Это был мой уровень понимания, когда я задумался над интересной задачей. Что если бы вы хотели иметь таблицу, которая автоматически заполняла бы строки на основе информации, предоставленной в других таблицах SQL? Как это можно сделать?
Примеры в этой статье относятся к боевому искусству бразильского джиу-джитсу. Для тех, кто совершенно не знаком с этим искусством, приведем основные сведения: Бразильское джиу-джитсу — это искусство борьбы. Один боец побеждает, поставив другого в положение, в котором он либо должен «подчиниться», либо рискует потерять сознание или получить какую-либо травму. Когда один из бойцов подчиняется таким образом, это называется «выходом из поединка». В бразильском джиу-джитсу большое внимание уделяется формализованным техникам и позициям. Например, самая известная позиция — «закрытая защита», когда один боец обхватывает другого ногами вот так:
Находясь в этой позиции, каждый боец имеет в своем распоряжении множество «атак». Соответственно, у другого бойца есть множество возможных «защит» на каждую атаку. Если бы мы попытались отобразить все эти приемы, то получили бы многоуровневое вложенное дерево данных, которое выглядит примерно так:
Где каждая позиция имеет множество атак, а каждая атака имеет множество защит.
Теперь, когда мы понимаем эту базовую структуру, давайте представим, что мы — новый ученик бразильского джиу-джитсу, который пытается уследить за всеми позициями, атаками и защитами, которые мы изучаем. Мы пытались записывать все это в блокнот, но потом вспомнили, что мы программисты, поэтому формат бумаги и ручки нам не подходит. Поэтому нам нужно создать базу данных SQL с помощью Active Record, чтобы отслеживать то, что мы изучаем.
Наша схема Active Record выглядит примерно так:
create_table "attacks", force: :cascade do |t|
t.string "name"
t.string "result"
t.string "notes"
t.string "image"
t.integer "position_id"
end
create_table "defenses", force: :cascade do |t|
t.string "name"
t.string "attack_id"
t.string "notes"
t.string "result"
t.string "stage"
end
create_table "positions", force: :cascade do |t|
t.string "name"
t.string "notes"
end
Кроме того, у нас есть следующие три ruby-модели:
# in Position.rb
class Position < ActiveRecord::Base
has_many :attacks
end
# in Attack.rb
class Attack < ActiveRecord::Base
has_many :defense
belongs_to :position
end
# in Defense.rb
class Defense < ActiveRecord::Base
belongs_to :attack
end
То есть мы создали 3 SQL-таблицы, по одной для позиций, атак и защиты. Кроме того, мы создали класс ruby для моделирования каждой из этих таблиц.
Отлично! Теперь мы можем легко хранить все техники, которые мы изучаем. Но теперь давайте предположим, что мы хотим изучить изучаемые техники, и мы хотим, чтобы компьютер предоставил нам возможную последовательность, которую мы могли бы практиковать. Например, мы хотим, чтобы программа сказала нам: «Начни с закрытой защиты, затем отработай удушающий прием «треугольник», и пусть твой противник защищается, пряча руку за твоей ногой». Когда мы выполняем подобный сценарий, это называется «последовательность». Более того, допустим, мы не хотим отрабатывать только одну последовательность, а хотим в течение определенного времени отработать ВСЕ возможные последовательности.
Здесь мы возвращаемся к нашей предыдущей задаче. Как попросить ruby автоматически сгенерировать список всех возможных перестановок техник, которые мы предоставили?
Во-первых, учтите тот факт, что по мере добавления техник количество возможных последовательностей может стать довольно большим. А в классической моде лучшим способом хранения больших объемов данных является таблица SQL. Поэтому мы начнем с создания новой SQL-таблицы под названием «последовательности» с помощью Active Record.
(Обратите внимание, я не стал подробно описывать настройку Active Record. Если вам нужна помощь в настройке, посмотрите эту статью: use-activerecord-in-your-ruby-project
Для создания новой миграции мы открываем терминал, переходим в каталог нашего проекта и набираем rake db:create_migration create_sequence_table
. Далее в файле миграции нам понадобится следующий код:
class CreateSequenceTable < ActiveRecord::Migration[6.1]
def change
create_table :sequences do |t|
t.integer :position_id
t.integer :attack_id
t.integer :defense_id
end
end
end
Наш класс наследуется от ActiveRecord::Base. «create_table» — это метод, который поставляется вместе с Active Record. Он использует блочный параметр |t|
и позволяет указать столько столбцов, сколько вы хотите, используя синтаксис t.integer :position_id
или t.string :name
, если ваша таблица будет иметь столбец name. В нашем случае, поскольку мы знаем, что будет много последовательностей для каждой позиции, атаки и защиты, мы должны указать столбец с целочисленным типом данных для каждого и использовать соглашение об именовании :position_id
. Это указывает ActiveRecord на сопоставление поля ‘id’ из таблиц position, attacks и defenses с этими столбцами соответственно.
Теперь, когда мы все настроили, нам нужно создать модель Active Record для соответствия нашей таблице ‘sequences’. Используя конвенцию Active Record, этот файл должен называться «Sequence.rb» и содержать следующий код:
require_relative '../../config/environment.rb'
class Sequence < ActiveRecord::Base
belongs_to :defense
belongs_to :position
belongs_to :attack
end
Далее нам нужно сообщить каждой из других таблиц, что у них «много» последовательностей, чтобы таблицы могли объединиться.
#in Position.rb
class Position < ActiveRecord::Base
has_many :attacks
has_many :sequences
end
#in Attack.rb
class Attack < ActiveRecord::Base
has_many :defense
has_many :sequences
belongs_to :position
end
#in Defense.rb
class Defense < ActiveRecord::Base
belongs_to :attack
has_many :sequences
end
Давайте сделаем шаг назад и вспомним, что именно мы хотим сделать. Мы хотим попросить программу заполнить таблицу последовательностей всеми возможными перестановками позиций, связанных с ними атак и связанных с ними защит.
Давайте продолжим и определим несколько таких приемов для использования в нашем примере.
#create a position called "closed_guard"
Position.create name:"closed_guard"
#create an attack called "triangle_choke" that belongs to closed_guard
Attack.create name: 'triangle_choke', Position.find_by(name: "closed_guard").id
#create a defense called "posture_up" that belongs to the attack "triangle_choke"
Defense.create name: "posture_up", attack_id: Attack.find_by(name:'triangle_choke').id
#create a defense called "hide_arm" that belongs to the attack "triangle_choke"
Defense.create name: "hide_arm", attack_id: Attack.find_by(name:'triangle_choke').id
#create an attack called "omoplata" that belongs to closed_guard
Attack.create name: 'omoplata', Position.find_by(name: "closed_guard").id
#create a defense called "shoulder_roll" that belongs to the attack "omoplata"
Defense.create name: "shoulder_roll", attack_id: Attack.find_by(name:'omoplata').id
#create a defense called "duck_under_leg" that belongs to the attack "omoplata"
Defense.create name: "duck_under_leg", attack_id: Attack.find_by(name:'omoplata').id
#Create another position called "open_guard"
Position.create name:"open_guard"
#create an attack called "scissor-sweep" that belongs to open_guard
Attack.create name: 'scissor-sweep', Position.find_by(name: "open_guard").id
#create a defense called "post_with_arm" that belongs to the attack 'scissor-sweep'
Defense.create name: "post_with_arm", attack_id: Attack.find_by(name:'scissor-sweep').id
#create a defense called "post_with_leg" that belongs to the attack 'scissor-sweep'
Defense.create name: "post_with_leg", attack_id: Attack.find_by(name:'scissor-sweep').id
#create an attack called "over_the_leg_guard_pass" that belongs to open_guard
Attack.create name: 'over_the_leg_guard_pass', Position.find_by(name: "open_guard").id
#create a defense called "shrimp_out" that belongs to the attack 'over_the_leg_guard_pass'
Defense.create name: "shrimp_out", attack_id: Attack.find_by(name:'over_the_leg_guard_pass').id
#create a defense called "pull_closed_guard" that belongs to the attack 'over_the_leg_guard_pass'
Defense.create name: "pull_closed_guard", attack_id: Attack.find_by(name:'over_the_leg_guard_pass').id
Это дает нам две позиции, каждая с двумя атаками, и каждая из этих атак с двумя защитами. Если мы подумаем о всех возможных последовательностях, используя эти данные, то получим:
closed_guard > triangle_choke > posture_up
closed_guard > triangle_choke > hide_arm
closed_guard > omoplata > shoulder_roll
closed_guard > omoplata > pull_out_arm
open_guard > scissor-sweep > post_with_arm
open_guard > scissor-sweep > post_with_leg
open_guard > over_the_leg_guard_pass > Shrimp_out
open_guard > over_the_leg_guard_pass > pull_closed_guard
Теперь встает вопрос, как заставить ruby автоматически заполнить таблицу «sequences» вышеупомянутыми последовательностями? Здесь мы используем тот факт, что модель Active Record на самом деле является просто классом ruby, что означает, что мы можем определять методы класса. Давайте определим один из них, который выглядит следующим образом:
require_relative '../../config/environment.rb'
class Sequence < ActiveRecord::Base
belongs_to :defense
belongs_to :position
belongs_to :attack
def self.generate
Position.all.each do |position|
position.attacks.each do |attack|
attack.defense.each do |defense|
self.create position_id: position.id, attack_id: attack.id, defense_id: defense.id
end
end
end
end
end
Мы назвали метод нашего класса «generate». Мы итерируем каждую защиту, каждую атаку, каждую позицию, и для каждой из них мы создаем экземпляр класса Sequence.
Теперь, если мы откроем консоль, запустим Sequence.generate и проверим нашу таблицу последовательностей с помощью Sequence.all, мы увидим полный список всех возможных последовательностей.
[#<Sequence:0x0000000110333f18
id: 13,
position_id: 33,
attack_id: 1,
defense_id: 141>,
#<Sequence:0x0000000110333e28
id: 14,
position_id: 33,
attack_id: 1,
defense_id: 142>,
#<Sequence:0x0000000110333d38
id: 15,
position_id: 33,
attack_id: 2,
defense_id: 143>,
#<Sequence:0x0000000110333c70
id: 16,
position_id: 33,
attack_id: 2,
defense_id: 144>,
#<Sequence:0x0000000110333ba8
id: 17,
position_id: 34,
attack_id: 3,
defense_id: 145>,
#<Sequence:0x0000000110333ae0
id: 18,
position_id: 34,
attack_id: 3,
defense_id: 146>,
#<Sequence:0x0000000110333
position_id: 34,
attack_id: 3,
position_id: 34,
attack_id: 3,
defense_id: 146>,
#<Sequence:0x0000000110333a18
id: 19,
position_id: 34,
attack_id: 4,
defense_id: 147>,
#<Sequence:0x0000000110333950
id: 20,
position_id: 34,
attack_id: 4,
defense_id: 148>]
Это замечательно, но нам действительно нужно это в формате, удобном для чтения. Поскольку мы будем считывать эти данные с сервера, давайте создадим английскую версию этого повествования в нашем файле application_controller.rb, указав новый маршрут под названием «/sequences».
#when we make a fetch request to "/sequences"
get "/sequences" do
#have narratives begin as an empty array
narratives = []
#for each sequence, build a json object containing a narrative by finding each position, attack and defense using their join key in the sequence table.
Sequence.all.each do |sequence|
narrative = {narrative: "Beginning in #{Position.find(sequence.position_id).name} position, one attacks with the #{Attack.find(sequence.attack_id).name} and the other defends with the #{Defense.find(sequence.defense_id).name} defense"}
#and add that narrative to the narratives array
narratives.push narrative
end
#convert narratives array to json and return it!
narratives.to_json
end
Таким образом, когда мы запустим наш сервер и сделаем запрос get к http://localhost:9292/sequences
, мы получим следующее:
// 20220720143954
// http://localhost:9292/sequences
[
{
"narrative": "Beginning in closed_guard position, one attacks with the triangle_choke and the other defends with the posture_up defense"
},
{
"narrative": "Beginning in closed_guard position, one attacks with the triangle_choke and the other defends with the hide_arm defense"
},
{
"narrative": "Beginning in closed_guard position, one attacks with the omoplata and the other defends with the shoulder_roll defense"
},
{F
"narrative": "Beginning in closed_guard position, one attacks with the omoplata and the other defends with the duck_under_leg defense"
},
{
"narrative": "Beginning in open_guard position, one attacks with the scissor-sweep and the other defends with the post_with_arm defense"
},
{
"narrative": "Beginning in open_guard position, one attacks with the scissor-sweep and the other defends with the post_with_leg defense"
},
{
"narrative": "Beginning in open_guard position, one attacks with the over_the_leg_guard_pass and the other defends with the shrimp_out defense"
},
{
"narrative": "Beginning in open_guard position, one attacks with the over_the_leg_guard_pass and the other defends with the pull_closed_guard defense"
}
]
Великолепно! Однако здесь есть одна проблема. Как это настроено сейчас, таблица последовательностей заполняется только тогда, когда мы запускаем Sequence.generate. Что произойдет, если мы узнаем новую защиту для triangle_choke и захотим добавить ее? Как тогда убедиться, что мы также добавим последовательность, соответствующую этой защите?
Один из способов сделать это — создать новый метод класса под названием create_with_sequence
. И если мы помним, метод класса «create» пришел из ActiveRecord::Base. Это означает, что внутри «create_with_sequence» мы можем вызвать self.create
. Мы также можем написать код, который удаляет все записи в таблице последовательностей, а затем регенерирует таблицу, теперь уже с новой Defense, которую мы только что создали, включенной в последовательности. Чтобы сделать это, давайте изменим наш файл модели Defense.rb следующим образом:
require_relative '../../config/environment.rb'
class Defense < ActiveRecord::Base
belongs_to :attack
has_many :sequences
def self.create_with_sequence args
self.create args
Sequence.all.each do |sequence|
sequence.destroy
end
Sequence.generate
end
end
Итак, теперь давайте создадим новую защиту в консоли rake
следующим образом:
Defense.create_with_sequence name: "tuck_arm_in_early", attack_id: Attack.find_by(name:'omoplata').id
Теперь мы должны увидеть новую последовательность, отображающуюся в нашем http://localhost:9292/sequences
.
"narrative": "Beginning in closed_guard position, one attacks with the omoplata and the other defends with the tuck_arm_in_early defense"
},
Одна из замечательных особенностей нашего дерева данных заключается в том, что новая последовательность определяется только при создании новой защиты, поэтому нам нужно только добавить этот код в наш файл Defense.rb.
Давайте сделаем несколько простых рефакторов, чтобы немного очистить этот файл. Для начала, давайте просто включим код для полной очистки таблицы последовательностей перед ее восстановлением в модели Sequence. Теперь наш Sequence.rb выглядит следующим образом:
require_relative '../../config/environment.rb'
class Sequence < ActiveRecord::Base
belongs_to :defense
belongs_to :position
belongs_to :attack
def self.generate
Sequence.all.each do |sequence|
sequence.destroy
end
Position.all.each do |position|
position.attacks.each do |attack|
attack.defense.each do |defense|
self.create position_id: position.id, attack_id: attack.id, defense_id: defense.id
end
end
end
end
end
А наш файл Defense.rb выглядит следующим образом:
require_relative '../../config/environment.rb'
class Defense < ActiveRecord::Base
belongs_to :attack
has_many :sequences
def self.create_with_sequence args
self.create args
Sequence.generate
end
end
Итак, мы достигли того, что поставили перед собой, мы создали таблицу, которая автоматически заполняется описательными последовательностями, используя данные из трех других таблиц.
Удачного кодирования!