Статьи

MongoDB и Grails

Совсем недавно у меня было требование хранить неструктурированные данные JSON, которые возвращались из веб-службы. Веб-сервис возвращал различные футбольные команды со всего мира. Среди данных, содержащихся в большинстве футбольных команд, был список футболистов, которые были частью команды. У некоторых из команд было 12 игроков, у некоторых было 20, у некоторых даже было больше 20. У игроков был свой атрибут, некоторые было легко предсказать, а некоторые невозможно. Что касается всей структуры данных, единственным атрибутом, который, как я знал, определенно будет возвращаться, было название команды. После этого все зависело от каждой команды.

?
{
"teams": [{
"teamname":"Kung fu pirates",
"founded":1962,
"players": [
{"name": "Robbie Fowler", "age": 56},
{"name": "Larry David", "age": 55}
...
]},
{
"teamname":"Climate change observers",
"founded":1942,
"players": [
{"name": "Jim Carrey", "age": 26},
{"name": "Carl Craig", "age": 35}
...
]},
...
]
}

Есть несколько разных способов хранения этих данных. Я решил пойти на MongoDB. Основные причины:

  • Я хотел сохранить данные как можно ближе к формату ответов JSON, которые я получал от веб-службы. Это будет означать меньше кода, меньше ошибок, меньше хлопот.
  • Я хотел что-то, что имело низкую кривую обучения, хорошую документацию и хорошую поддержку отрасли (потоки stackoverflow, сообщения в блогах и т. Д.)
  • Кое-что, у которого был плагин Grails, который был задокументирован, имел шаг и выглядел так, как будто его поддерживали
  • Такие функции, как  текстовые stemming,  были приятны. Некоторая поддержка была бы хорошей, но это не должно было сокращать возраст.
  • Поддержал бы хорошие средства поиска JSON, индексацию и т. Д.

MongoDB поставил галочку во всех клетках. Вот так у меня все получилось. После того, как я установил MongoDB в соответствии с инструкциями Mongo  и  плагином MongoDB Grails , пришло время написать некоторый код. Теперь вот аккуратная часть, почти не было кода. Я создал объект домена для Команды.

?
class Team implements Serializable {
static mapWith = "mongo"
static constraints = {
}
static mapping = {
teamname index: true
}
String teamname
List players
static embedded = ['players']
}

Относительно объекта домена Team:

  • Первое, что я хотел сказать об объекте домена Team, это то, что мне даже не нужно было его создавать. Причина, по которой я использовал этот подход, заключалась в том, что я мог использовать API- интерфейсы в стиле GORM, такие как  Team.find (),  если бы захотел.
  • Игроки это просто список объектов. Я не удосужился создать объект Player. Мне нравится идея всегда обеспечивать, чтобы игроки для команды всегда были в структуре данных List, но я не видел необходимости вводить что-либо еще.
  • Игроки помечены как  встроенные . Это  означает,  что команда и игроки хранятся в единой денормализованной структуре данных. Это позволяет, помимо прочего, получать и обрабатывать данные группы в одной операции базы данных.
  • Я пометил название команды в качестве индекса.
  • Я пометил объект домена как
    static mapWith = "mongo"

    Это означает, что, если я также использовал другое решение для персистентности с моим GORM (postgres, MySQL и т. Д.), Я говорю GORM, что этот класс домена Team предназначен только для Mongo — держите свои реляционные руки подальше от него. Смотрите здесь  для информации.  Примечание.  Это хорошее напоминание о том, что GORM — это более высокий уровень абстракции, чем спящий. Возможно иметь объект GORM, который не использует hibernate, но вместо этого отправляется в хранилище NoSQL и не приближается к hibernate.

Вы заметите, что в JSON есть такие атрибуты команды, как  found  , которые не были явно объявлены в классе Team. Именно здесь Groovy и NoSQL очень хорошо играют друг с другом. Мы можем использовать некоторые возможности метапрограммирования Groovy для динамического добавления атрибутов к объекту домена Team.

?
private List<team> importTeams(int page) {
def rs = restClient.get("teams") // invoke web service
List<team> teams = rs.teams.collect {
teamResponse ->
Team team = new Team(teamname: teamResponse.teamname)
team.save(); // Save is needed to dynamically add the attribute
teamname.each {key, value ->
team["$key"] = value
}
teamname.save(); // We need the second save to ensure the variants get saved.
return teamname
}
log.info("importTeams(),teams=teams);
teams
}

Итак, основные моменты в нашем методе importTeams ()

  • После получения ответа JSON мы запускаем функцию сбора данных в массиве groups. Это создаст объекты домена Team.
  • Мы используем метапрограммирование для динамического добавления любого атрибута, который возвращается в структуре команды JSON, к объекту Team. Примечание.  Сначала нужно  вызвать save (), чтобы иметь возможность динамически добавлять атрибуты, объявленные в объекте домена Team, в объект домена Team. Мы также должны снова вызвать save (), чтобы гарантировать, что атрибуты, объявленные в объекте домена Team, гарантируют, что они сохранены. Это может измениться в будущих версиях плагина MongoDB, но это то, что мне нужно было сделать, чтобы он работал (я использовал плагин MongoDB версии 3.0.1)

Так что же дальше? Напишите несколько запросов. Итак, два варианта здесь. Во-первых, вы можете использовать динамические поиски и запросы критериев с GORM благодаря плагину MongoDB. Но я этого не делал. Почему? Я хотел написать запросы как можно ближе к тому, как они должны быть написаны на монго. Для этого было несколько причин:

  • Утечка абстракции здесь неизбежна. Рано или поздно вам придется написать запрос, который GORM не очень хорошо выполнит. Лучше подойти к этому головы.
  • Я хотел сначала выполнить запросы в консоли Mongo, проверить планы объяснения, если нужно, а затем использовать тот же запрос в своем коде. Проще сделать это, если я напишу запрос напрямую, не беспокоясь о том, что собирается делать GORM.

Общий формат запросов:

teams = Team.collection.find(queryMap) // where queryMap is a map of fields and the various values you are searching for. 

Хорошо, несколько примеров запросов …

?
Team.collection.find(["teamname":"hicks"]) // Find a team name hicks
Team.collection.find(["teamname":"hicks", "players.name": "Robbie Fowler"] // As above but also has Robbie Fowler
Team.collection.find(["players.name": "Robbie Fowler"] // Any teams that has a Robbie Fowler
Team.collection.find(["teamname":"hicks", "players.name": "Robbie Fowler", {"players.$":1}]  // Returns matching player only
Team.collection.find(["teamname":"/ick/"]) // Match on the regular expression /ick/, i.e. any team that contains text ick.

Что-нибудь еще? Да, конечно. Я хотел подключиться к экземпляру Mongo на своей машине в процессе разработки, но к машине Mongo на выделенном сервере в других средах (CI, stage, production). Для этого я обновил свой DataSource.groovy как:

?
environments {
development {
grails {
mongo {
host = "localhost"
port = 27017
username = "test"
password = "test"
databaseName = "mydb"
}
}
dataSource {
dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', ''
url = "jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000"
}
}
ci {
println("In bamboo environment")
grails {
mongo {
host = "10.157.192.99"
port = 27017
username = "shop"
password = "shop"
databaseName = "tony"
}
}
dataSource {
dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', ''
url = "jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000"
}
}
}

Вы увидите, что я настроил несколько источников данных (MongoDB и PostGres). Я не защищаю использование MongoDB и реляционной базы данных, просто указываю, что это возможно. Другое дело, что конфигурация MongoDB всегда находится под: grails {mongo {

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