Автозаполнение SQL-таблиц с помощью Active Record

Если вы читаете эту статью, то, вероятно, уже знакомы с основами, касающимися таблиц 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


Войти в полноэкранный режим Выход из полноэкранного режима

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

Удачного кодирования!

Оцените статью
devanswers.ru
Добавить комментарий