Seed файл и вы
10 Mar 2014Совсем недавно, на работе, потребовалось мне заполнить новый проект данными для дальнейшего тестирования и разработки. Конечно же, данные должны быть в любом виде, и первое, о чем я подумал, был 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крипта, избавшись от явного повтора кода.
На этом, пожалуй, я закончу наши эксперименты. Как видите, простора для фантазии осталось еще много и также осталось много идей для рефакторинга. В любом случае, данный пример явно показывает, что к любой, сколь скучной она не была бы, задаче всегда можно применить творческий подход и неплохо развлечься :)