Seed файл и вы

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

Обычная практика многих людей - задать одинаковые данные для всех типов данных и наплодить их с десяток. Смотрится это обычно как-то так:

user = {
  name:  'Jon'
  email: 'my@email.org'
  password: '12345678',
  password_confirmation: '12345678'  
  }

post = {
  title:  'My Post'
  body:   'My body'
  }

comment = { body: 'comment' }

10.times do
  my_user = User.create(user)
  my_post = my_user.create_post(post)
  my_post.create_comment(user, comment)
end

Но согласитесь, это скучно, банально и задевает чувство прекрасного. Поэтому давайте плюнем на все и развлечемся, создав свой собственный, изменяющийся из раза в раз мир :)

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

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

user = {
  name:  admin
  email: admin@my_app.com
  password: '12345678',
  password_confirmation: '12345678'  
  }

rnd = Random.new
user_count = rnd.rand(18..23)
User.create(user)

user_count.times do
  user[:name]  = Faker::Name.name
  user[:email] = Faker::Internet.email  
  User.create(user)
end

Cобственно я уверен, faker поможет вам сгенерировать почти любую информацию, стоит только открыть доки. Ну а если вам не угодил этот гем, то существует достаточно много других data генераторов.

Не думаю, что тут что-то было сложно, поэтому пререйдем к постам. Сказать по правде, в нашем проекте посты состояли из строго заданных кусков html-a, поэтому тут ничего не оставалось, кроме как делать в лоб. Единственный момент, мы будем выбирать произвольно пользователя, чтобы от его имени создавать наш пост:

posts = [
  {
    title:  'My first Post'
    body:   'My body'
  },
  {
    title:  'My second Post'
    body:   'My body'    
  },
  # Еще какое-то количество данных для постов ...
]

def rnd_user(count, rnd)
  random_user_id = rnd.rand(1..(count))
  User.find(random_user_id)
end

posts.each do |post|
  rand_user = rnd_user user_count, rnd

  post[:user_id] = rand_user.id
  created_post = rand_user.create_post(post)
end

Настало время самого интересного и забавного, комментарии. В данном проекте мы использовали гем acts_as_commentable_with_threading. Он содержит 2ух уровневую структуру комментариев, поэтому работы нам немного прибавилось. Чтобы создать комментарий, нам необходимы 3 значения: пост, где будет этот комментарий, пользователь, оставивший комментарий, и непосредственно сам текст комментария. Смотрится все это примерно так:

post.build_comment(user_id, body)

Ну а для “подкомментария” нам так же необходимо знать родительский комментарий, от которого ветка и пойдет, т.е. создание подобного комментария будет выглядеть примерно так:

child_comment = post.build_comment(user_id, body)
child_comment.move_to_child_of(comment)

А теперь давайте создадим от 10 до 21 главных комментариев и до 9ти дочерних для каждого главного, при этом каждый комментарий будет оставлять рандомный пользователь:

posts.each do |post|
  rand_user = rnd_user user_count, rnd

  post[:user_id] = rand_user.id
  created_post = rand_user.create_post(post)

  rnd.rand(10..21).times do
    rand_user = rnd_user user_count, rnd
    comment = created_post.build_comment(rand_user.id, 'Comment body')
    comment.save!

    rnd.rand(9).times do
      rand_user = rnd_user user_count, rnd
      child_comment = created_post.build_comment(rand_user.id, 'Comment body')
      child_comment.save!
      child_comment.move_to_child_of(comment)
    end
  end
end

Хм, рандомное количество комментариев мы сделали, пользователей тоже разных назначили, но вот незадача, у нас body каждого комментария одно и тоже, а именно 'Comment body'.Что же делать и как нам быть? Раз уж мы договорились создать подобие “живого” приложения, то и комментарии у нас должны быть разные и тоже живые. Первое, что приходит в голову, - опять использовать массив данных, но я слишков ленив (да и не путь самурая это), чтобы все это набирать, пусть даже копипастить и тем более придумывать. Второе, что приходит на ум, генерировать рандомную строчку текста. Да, идея не плохая, как минимум, нам придется писать меньше кода, и он по-любому будет всегда разный. Но есть одно но: мы пытаемся достигнуть абсолютной правдоподобности, а строки вида 'skjafnskdjn ksajdnf' нам точно не подойдут как комментарии. Поэтому нам на помощь приходит отличное решение - гем raingrams.

Что же такого может этот гем, спросите Вы? На самом деле, ничего особенного, Вы просто скармливаете ему текст, а он, в свою очередь, разбивает его на куски и рандомно выдает обратно. В чем плюсы? Да, они не отличаются от банальной генерации строки, единственное и очевидное отличие - генерируемый текст будет логичен в пределах строки.

В документации достаточно подробно описано, как гем ставится и настраивается, но я бы хотел уделить внимание 2ум подводным камням, с которыми мы столкнулись:

  • Во первых, гем не поддерживает русский язык. Скажем так, он его не видит. Поэтому, если для Вас важен русский язык, используйте наш форк, в котором исправлен этот косяк.

  • Ну а второй момент, в старых версиях существовал метод train_with_url, в который передавалась ссылка, а он уже все парсил и выдавал конечный результат. К сожалению, в свежих версиях этот метод был убран, причем убран очень хитро. Если быть точным, то автор просто вырезал часть этого метода, а вторую забыл(а может, решил стебануться над простыми парнями как мы, этого я, к сожалению, не знаю :) ).

А теперь, используя полученные знания, перепишем наш метод. В качестве текста для raingrams мы будем использовать комментарии из пикабу, которые предварительно распарсим:

model = QuadgramModel.build do |model|
  doc = Nokogiri::HTML(open('http://pikabu.ru/story/v_den_programmista_pro_logiku_pikabu_685289'))
  doc.search('div.comment_desc').each do |div|
    model.train_with_text(div.inner_text)
    model.refresh
  end
end

# ....

posts.each do |post|
  rand_user = rnd_user user_count, rnd

  post[:user_id] = rand_user.id
  created_post = rand_user.create_post(post)

  rnd.rand(10..21).times do
    rand_user = rnd_user user_count, rnd
    comment = created_post.build_comment(rand_user.id, model.random_sentence)
    comment.save!

    rnd.rand(9).times do
      rand_user = rnd_user user_count, rnd
      child_comment = created_post.build_comment(rand_user.id, model.random_sentence)
      child_comment.save!
      child_comment.move_to_child_of(comment)
    end
  end
end

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

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

Как думаете, где еще нам придется создавать пользователей (и не только их), которых мы создали в самом начале? Правильно, в тестах, надо же на чем-то тестировать приложение. Так почему бы нам не убить 2ух зайцев и не заменить ручную генерацию, как это было в начале статьи, на старую добрую фабричную? Так как в нашем проекте мы используем гем fabrication, то и пример будет с ним. Вы также можете использовать любую другуюю фабрику, которая вам по вкусу.

Для начала определим нашего пользователя и администратора:

Fabricator(:user) do
  email Faker::Internet.email
  name Faker::Name.name
  password '12345678'
  password_confirmation '12345678'
end

Fabricator(:admin) do
  email 'admin@my_app.org'
  name 'admin'
  password '12345678'
  password_confirmation '12345678'
end

Ну а теперь, воспользуемся нашей новосозданной фабрикой для избавления от лишнего кода в seed файле:

user_count = rnd.rand(18..23)
Fabricate(:admin)

user_count.times { Fabricate(:user) }

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

На этом, пожалуй, я закончу наши эксперименты. Как видите, простора для фантазии осталось еще много и также осталось много идей для рефакторинга. В любом случае, данный пример явно показывает, что к любой, сколь скучной она не была бы, задаче всегда можно применить творческий подход и неплохо развлечься :)