Страницы

Поиск по вопросам

четверг, 18 апреля 2019 г.

Scala. Список футур. Выполнение шаг за шагом

Хола, коллеги! У меня есть список некоторых сущностей. Они могут быть вложены друг в друга. Например:
case class Entity(id: Long, parentId: Long)
val list = List( Entity(1,0), Entity(2,1), Entity(3,2), Entity(4,1), Entity(5,0) )
Мне нужно добавить их в БД. Мой сервис:
val listOfFutures = list map createEntity // createEntity - мой метод, // который добавляет сущность в БД. // Возвращает Future[Entity] Future.sequence( listOfFutures ) map { _ => println("Ok!") }
Проблема в constraints(Как их по-русски назвать?). Получаю ошибку:
Create failed. No such parent 1 entity exists.
Я думаю, что проблема в том, что метод createEntity выполняется параллельно. Как заставить этот код выполняться последовательно, сущность за сущностью?


Ответ

Future считается монадой, так как у неё есть последовательный flatMap
list.foldLeft(Future.successful(()))(( prevF, entity) => prevF .flatMap( _ => createEntity(entity)) .map( _ => ()) )
Аналогичный код, используя for-comprehension:
list.foldLeft(Future.successful(()))(( prevF, entity) => for { _ <- prevF _ <- createEntity(entity) } yield () )
Объяснение:
val entity = Entity(0, 1) createEntity(entity).map( _ => println("Ok!"))
Метод map запоминает полученную функцию и выполняет её после того как получит успешный результат Future
Meтод flatMap работает также, только ожидает что полученная функция тоже будет возвращать Future
createEntity(entityOne).flatMap( _ => createEntity(entityTwo))
Таким образом мы соединяем две Future последовательно, т.е. вторая вызовется только после успешного выполнения первой.
Отсюда очевидно что мы можем выстраивать длинные последовательные цепочки.
createEntity(entityOne) .flatMap( _ => createEntity(entityTwo)) .flatMap( _ => createEntity(entityThree)) .flatMap( _ => createEntity(entityFour))
А результатом будет новая Future. Ок, а как поступить если у нас есть список Future? Так же как мы бы поступили бы например с числами.
val listNumbers = List(1, 2, 3, 4, 5)
val zeroNumber = 0 def joinNumbers(a: Int, b: Int): Int = a + b
listNumbers.foldLeft(zeroNumber)(joinNumbers)
Почему foldLeft, а не map? Потому что прибавляем по очереди, и нам нужна сумма предыдущих чисел. Зачем нужен zeroNumber? Ну а вдруг список пустой - вернем 0.
Теперь с Future:
val listEntities: List[Entity] = ???
val zeroFuture = Future.successful(()) def joinFuture(prevF: Future[Unit], entity: Entity): Future[Unit] = prevF .flatMap(_ => createEntity(entity)) // тут вторая Future .map( _ => ())
listEntities.foldLeft(zeroFuture)(joinFuture)
Следующий createEntity выполняется после предыдущих. Зачем нужен zeroFuture - ну а вдруг список пустой. UPD:
Попробую подробней расписать почему foldLeft, а не map. Сначала взглянем на map
val listOfFutures = list.map( entity => createEntity(entity))
map просто проходит по листу и вызывает создание Энтити. Он не ждёт пока создание завершится, он просто создаёт новую Future и идёт дальше к следующему элементу листа.
Т.е. на каждой итерации нам нужно "нечто" что будет знать о том что на предыдущем шаге Энтити создался:
val listOfFutures = list.map( entity => val prevEntityWasCreated = ??? // что-то, что знает о предыдущем Энтити
// flatMap заставляет выполнятся ПОСЛЕ prevEntityWasCreated.flatMap(_ => createEntity(entity)) )
Т.е. проходимся по списку и на каждом этапе ждем пока завершиться предыдущий. Что-то подобное нам надо, верно?
Точнее такое:
val listOfFutures = list.map( (prevEntityWasCreated, entity) =>
prevEntityWasCreated.flatMap(_ => createEntity(entity)) )
Супер. Второй энтити ждет когда создастся первый, третий ждёт когда создастся второй. А чего ждёт первый? Надо создать пустую Future специально для первой этнити:
val zeroFuture = Future.successful(()) val listOfFutures = list.map(zeroFuture)( (prevEntityWasCreated, entity) =>
prevEntityWasCreated.flatMap(_ => createEntity(entity)) )
Всё отлично, только тип не совпадает:
val zeroFuture: Future[Unit] = Future.successful(()) val foo: Future[Entity] = prevEntityWasCreated.flatMap(_ => createEntity(entity))
Просто добавим map и изменим тип на одинаковый
val zeroFuture: Future[Unit] = Future.successful(()) val foo: Future[Unit] = prevEntityWasCreated.flatMap(_ => createEntity(entity)).map( _ => ())
Последнии штрих - переименовать новую функцию в foldLeft
list.foldLeft(Future.successful(()))(( prevF, entity) =>
val foo: Future[Unit] = prevF .flatMap( _ => createEntity(entity)) .map( _ => ()) // возвращаем Future для следующего элемента foo )

Комментариев нет:

Отправить комментарий