Как стать автором
Обновить

Play framework + Scala — from zero to hero

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


Итак. Вы собрались создать сайт. Сайт, которым будут пользоваться миллионы. У Вас есть потрясающая, оригинальная идея. Конечно же, Вы уже знакомы с веб программированием, возможно даже очень хорошо знакомы. Но Вы же знаете, что от технологий зависит успешность Вашего проекта, и Вы очень боитесь прогадать. Уже второй день вы сидите возле монитора, сравнивая технологии, читая комментарии, смотря презентации. Вы не можете ни есть, ни спать. Именно для Вас я написал эту статью. Что же я порекомендую вам? А вот что: Play framework 2.1.* + Scala.

История


Лично мой путь, как backend разработчика был таков: php(Yii) -> RoR -> Play. Я думаю, не надо объяснять первую стрелочку, а про вторую я расскажу. До разработки на php я разрабатывал на java. Правда не EE, я разрабатывал gui на swing. Когда пришлось переквалифицироваться в backend разработчика, первая мысль была: java. Возможно, если бы я увидел Play еще тогда, то не было бы цепочки php -> RoR -> Play. Но для того времени play был еще сыроват, да и сейчас я не использую его с java. Но беглый взгляд на ЕЕ, и она была отброшена (видимо, не для меня это — enterprise). Ну а по поводу второй стрелочки. Почему же я ушел с RoR, с рельсов, которые все так хвалят? Ответ:
  • из-за производительности
  • из-за языка

Несмотря на то, что у меня есть несколько проектов на RoR, самим языком я владею недостаточно хорошо. И когда появлялись проблемы, решение которых уходило дальше работы с фреймворком (математические вычисления и т.д.), то мне надо было все дальше погружаться в язык. А я этого не хотел. И вот, в какой-то момент, бороздя просторы интернета, я нашел чудо, под названием Scala.
Я не хочу, чтобы статья превратилась в холивар между императивным и функциональным программированием (да и вообще не в какой холивар). Даже если вас не устраивает Scala — вполне можно использовать play с java, хотя я бы порекомендовал каждому джависту (да и не только джавистом) познакомиться с этим языком. Почему я выбрал его, а не руби? Во-первых, он выполняется JVM, что дает доступ из него ко всему, что доступно из java. Во-вторых, он выполняется быстрее чем руби (благодаря JVM). Так зачем же, скажете Вы, тебе скала, просто используй java. Дело в том, что почти во всех случаях, и в самом фреймворке play, и не только код на скале будет занимать меньше места, чем код javа. И при этом, заметьте, производительность не хуже, возможно даже скала немного шустрее. Собственно, поэтому я и выбрал скалу. Даже если вы никогда не видели код на scale, с этим туториалом у вас не должно возникнуть осложнений.

Итак, давайте же посмотрим, что из себя представляет play.

Начало



Прошу простить меня пользователей windows, но так как у меня debian, я буду предоставлять решения для пользователей unix. Надеюсь, вы разберетесь.

Подготовка:
  1. Вам потребуется JDK 6 или выше.
  2. Play binary package


У нас все готово, можно приступать к созданию приложения. В этом уроке мы с вами создадим простенькое todo-приложение.

$ mkdir playapps && cd playapps
$ play new todolist




Теперь пару слов о среде разработки. Вы можете пользоваться любой IDE, или любимым текстовым редактором. Я буду пользоваться intellij idea.

$ cd todolist
$ play
[todolist] $ run


Мы запустили наше приложение в режиме разработки (development mode). Перейдя по адресу localhost:9000. Если вы все правильно сделали, то должны увидеть вот это:



Введение в Play и в Scala (частично)



Пора бы посмотреть, что было сгенерировано командой «play new todolist»
Если вы используете idea, то остановите сервер (Ctrl + D), и введите
[todolist] $ idea

Теперь, когда все нужные модули для idea созданы, мы можем открыть наш проект в IDE (этот шаг только для пользователей Intellij Idea).
Мы видим структуру проекта:



Самый важные для нас папки, это app (где лежат все компоненты MVC), и conf (с конфигом приложения и routes). Если вы уже знакомы с MVC, то можете немножко поэкспериментировать. Но для начала разберемся, почему, когда мы заходим на localhost:9000, то видим не пустую страницу.
Зайдем в conf/routes, видим там:
# Home page
GET     /                           controllers.Application.index

Исходя из этого, идем в app/controllers/Application.scala:
package controllers

import play.api._
import play.api.mvc._

object Application extends Controller {
  
  def index = Action {
    Ok(views.html.index("Your new application is ready."))
  }
  
}

Что же это за магическая строчка в def index, которая позволяет нам увидеть страницу, полную информации. Давайте розбираться. Но для начала хочу объяснить (для тех, кто не знаком с Scala). Запись типа
def index = Action {}

означает, что функция возвращает Action (это часть фреймворка, не будем углубляться; просто скажу, что Action обрабатывает запрос, и отправляет ответ в браузер). Да, и еще. Как вы уже успели заметить, в Scala точки с запятой необязательны (только если вы записываете несколько выражений в одну строку, например:
if(a>b) var c =5; var d=15 else var f = 10

).
Теперь разберемся с строкой
Ok(views.html.index("Your new application is ready."))

Первое: каждое выражение в скале имеет свое значение. Так функция index возвращает значение этой единственной строки.
Второе: разберем саму строку. Функция «Ok» создает «200 OK» ответ, заполненный html контентом, из view`а под названием index.scala.html (дефолтные views в play на Scala template language; если вы хотите — можете использовать haml, или еще что-то). Давайте откроем app/views/index.scala.html:
@(message: String)

@main("Welcome to Play 2.1") {
    
    @play20.welcome(message)
    
}


Первая строка здесь — сигнатура функции. А далее можно писать html код, с вставками scala (которые начинаются с символа @).
Теперь вы можете снова запустить сервер в режиме разработки и немного поэксперементировать, но перед тем как продолжить отмените все изменения.

Развитие событий



А теперь мы начнем создавать todo приложение.

Шаг первый: Routes & Controller

Для начала настроим файл conf/routes:
# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Home page
GET     /                                      controllers.Application.index

# Map static resources from the /public folder to the /assets URL path
GET     /assets/*file                 controllers.Assets.at(path="/public", file)
                   
# Tasks          
GET       /tasks                          controllers.Application.tasks
POST    /tasks                          controllers.Application.newTask
POST    /tasks/:id/delete        controllers.Application.deleteTask(id: Long)
POST    /tasks/:id/complete   controllers.Application.completeTask(id: Long)

Кроме стандартных, мы добавили еще 4 rout'a. Они позволят нам просматривать задания (первый), создавать задания (второй), удалять задания (третий), и делать задание завершенным (четвертый).

Теперь для каждого пути надо создать действия в app/controllers/Application.scala:

  def tasks = TODO

  def newTask = TODO

  def deleteTask(id: Long) = TODO

  def completeTask(id: Long) = TODO


Мы пока еще не знаем, что будут делать эти функции, поэтому мы используем встроенную функцию TODO, которая вернет HTTP ответ «501 Not Implemented». Да, и еще: Scala — строго типизированный язык, но объявление метода переменных может быть непривычно для Java/C++ программистов.
Java код:
int a = 15;
String s = "Hello, Habr!";

Scala код:
val a = 15 // Или val a: Int = 15
val s = "Hello, Habr!" // Или val s: String = "Hello, Habr!"


Давайте теперь попробуем запустить сервер, и посмотрим, что вышло.
[todolist] $ run

Заходим на localhost:9000/tasks, и видим:



Маленькое отступление: мы не хотим, чтобы пользователи нашего приложения видели страницу, которая генерируется функцией index контроллера Action, поэтому внесем в контроллер (app/controllers/Application.scala) маленькие изменения:

  def index = Action {
    Redirect(routes.Application.tasks)
  }


Функция Redirect перенаправит нас на
GET     /tasks                  controllers.Application.tasks


Шаг второй: Model


Как вы уже заметили, папки controllers и views сгенерировались, а папку models нам придется создать вручную. В корне приложения из консоли:
$ mkdir app/models


Теперь создадим в ней файл Task.scala. Он и станет нашей моделью. Итак, app/models/Task.scala:

package models

case class Task(id: Long, label: String, who: String, mytime: String, ready: Short)

object Task {

  def all(): List[Task] = Nil

  def create(label: String, who: String, time: String) {}

  def delete(id: Long) {}

  def complete(id: Long) {}

}


Обратите внимание, запись
case class Task(id: Long, label: String, who: String, mytime: String, ready: Int)

равноценна такой записи на java:
public class Task{
private long id;
private String label;
private String who;
private Int ready;

public Task(long id, String label, String who, Int ready){
this.id = id;
this.label = label;
this.who = who;
this.ready = ready;
}

public long getId(){
return id;
}
public void setId(long id){
this.id = id;
}
//и еще 3 геттера и сеттера


А теперь давайте немного отвлечемся к модели, и перейдем к

Третий шаг: Views


Изменим содержимое файла app/views/index.scala.html на:

@(tasks: List[Task], taskForm: Form[(String, String)])

@import helper._

@main("Todo list") {

    <h1>@tasks.size idea(s)</h1>

    <table style="text-align: center; border: 1px double black; width: 100%;">
    <tr><th>Idea</th><th>Who</th><th>When</th><th>Status</th><th>Complete?</th></tr>
    @tasks.map { task =>
        @if(task.ready == 0) {
            <tr>
            }else{
            <tr style="color: red; font-weight: bold; font-size: 16px;">
            }
    <td>@task.label</td>
    <td>@task.who</td>
    <td>@task.mytime</td>
    <td>@if(task.ready==0) {
        unfinished
    } else {
        finished
    }</td>
    <td>
        <table align=center>
            <td>@form(routes.Application.deleteTask(task.id)) {
                <input type="submit" value="Delete" onclick="return confirm('Are you sure?');">
                }</td>
            @if(task.ready==0){
                <td>@form(routes.Application.completeTask(task.id)){
                    <input type="submit" value="done?" onclick="return confirm('Are you sure?');">
                    }</td>
            }
        </table>
    </td>
    </tr>
    }
    </table>

    <h2>Add a new idea</h2>

    @form(routes.Application.newTask) {

        @inputText(taskForm("label"))
        @inputText(taskForm("who"))

        <input type="submit" value="Create">
    }
}


Обратите внимание на:

В первой строчке мы добавили в сигнатуру 2 параметра: список заданий, и «какой-то странный» taskForm. О нем мы поговорим чуть позже.
Далее идет импорт. Надо отметить, что запись в scala «import helper._» аналогична записи на java «import helper.*». Этот импорт предоставляет нам функцию form, которая создает тег с атрибутами action и method, а также функцию inputText, который создает несколько полей: input для ввода, label объясняет, что вводить, и еще label`ы, которые сообщают об ошибках.
Все остальные записи после @, на мой взгляд, интуитивно понятны.

Перейдем к «неведомой» taskForm. Внесем некоторые модификации в файл app/controllers/Application.scala:

  • Добавим пару импортов для роботы с формами:
    import play.api.data._
    import play.api.data.Forms._
    

  • Изготовим саму taskForm:
      def completeTask(id: Long) = TODO
    
      val taskForm = Form(
        tuple (
          "label" -> nonEmptyText,
          "who" -> nonEmptyText
        )
      )
    



Четвертый шаг: все вместе


Наступило время продолжить работу над главной страницей (где выводится список всех дел, а также форма для добавления дел).

Шаблон для нее у нас уже изготовлен (app/views/index.scala.html). Теперь надо доработать контроллер и модель. Начнем мы с контроллера:
  • добавим импорт модели в контроллер
    import models.Task
    
  • закончим работу с def tasks
    def tasks = Action {
      Ok(views.html.index(Task.all(), taskForm))
    }
    



То, что мы передаем в views.html.index() — мы передаем в функцию, сигнатура которой находится на первой строке файла index.scala.html. Сейчас Вы можете проверить результаты нашей незаконченной работы. Откройте в браузере localhost:9000/tasks, и, если вы все делали правильно, вы увидите:



Вы можете попробовать нажать кнопку «Create», но мы еще не написали def newTask, да и базы данных для хранения информации у нас еще нет.
Для начала закончим def newTask, которая будет обрабатывать данные из формы taskForm:

  • Для начала добавим пару импортов (для даты)
    import java.util.Calendar
    import java.text.SimpleDateFormat
    

  • а теперь напишем саму функцию newTaks
    def newTask = Action { implicit request =>
        taskForm.bindFromRequest.fold(
          errors => BadRequest(views.html.index(Task.all(), errors)),
          x=>x match { case(label,who) => {
            // Получаем текущее время
            val today = Calendar.getInstance().getTime()
            val timeFormat = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss")
            val time = timeFormat.format(today)
            //----------------------------
            Task.create(label, who, time)
            Redirect(routes.Application.tasks)
            }
          }
        )
      }
    



Для человека, не знакомого с Scala, синтаксис данного кода будет понятно трудно, поэтому я просто объясню, что он делает. Из запроса мы получаем данные формы, которые мы обрабатываем (функцией bindFromRequest.fold()). Если есть ошибки в запросе, то выполняется функция BadRequest, которая останавливает процесс, возвращает нас на главную страницу и выводит все ошибки. Далее нам надо обработать еще и само задание (label), и того, кто создал идею (who). А также мы находим время создания (заметьте, оно не отправляется с формой, просто записывается время создания задания), и вызываем функцию Task.create(). Как и функцию Task.all, мы скоро допишем в файле app/models/Task.scala. Ну и теперь, когда все создано, мы переправляем пользователя на главную страницу.

Наступило время обратиться к базе данных.

Что нам осталось сделать? Подключиться-таки к базе данных, дописать методы модели и контроллера. Займемся базой данных.

  1. В файле conf/application.conf раскомментируйте или добавьте строки
    db.default.driver=org.h2.Driver
    db.default.url="jdbc:h2:mem:play"
    

    Как вы уже поняли, мы будем использовать базу данных H2.
  2. В фреймворке play существует система еволюций. Они используются для создания структуры базы данных. Создайте файл conf/evolutions/default/1.sql с таким содержанием:
    # Tasks schema
     
    # --- !Ups
    
    CREATE SEQUENCE task_id_seq;
    CREATE TABLE task (
        id integer NOT NULL DEFAULT nextval('task_id_seq'),
        label varchar(2000),
        who varchar(40),
        mytime varchar(100),
        ready integer
    );
     
    # --- !Downs
     
    DROP TABLE task;
    DROP SEQUENCE task_id_seq;
    

    Сейчас настало самое время открыть страницу localhost:9000/tasks в браузере.
    Вы должны увидеть такую картинку:



    Play нашел нашу еволюцию, и хочет ее выполнить. Нажмите «Apply this script now».
  3. Для «связи» с базой данных мы будем использовать anorm для того, чтобы преобразовать JDBC сырой ResultSet в значение Task. Для начала добавим импорты:
    import anorm._
    import anorm.SqlParser._
    

    А теперь добавим основной код:
    val task = {
        get[Long]("id") ~
          get[String]("label") ~
          get[String]("who") ~
          get[String]("mytime") ~
          get[Int]("ready") map {
          case id~label~who~mytime~ready => Task(id, label, who, mytime, ready)
        }
      }
    

    Тут task выступает парсером значений, которые мы будем получать от JDBC ResultSet
  4. Теперь, когда парсер написан, мы можем смело приступать к подсоединению к базе данных. Добавим импорты:
    import play.api.db._
    import play.api.Play.current
    

    А теперь заканчиваем функции all, create, delete и complete:

      def all(): List[Task] = DB.withConnection { implicit c =>
        SQL("select * from task").as(task *)
      }
    
      def create(label: String, who: String, mytime: String) {
        DB.withConnection { implicit c =>
          SQL("insert into task (label,who,mytime,ready) values ({label},{who},{mytime}, 0)").on(
            'label -> label,
            'who -> who,
            'mytime -> mytime
          ).executeUpdate()
        }
      }
      
      def delete(id: Long) {
        DB.withConnection { implicit c =>
          SQL("delete from task where id = {id}").on(
            'id -> id
          ).executeUpdate()
        }
      }
    
      def complete(id: Long) {
        DB.withConnection { implicit c =>
          SQL("update task set ready=1 where id = {id}").on(
            'id -> id
          ).executeUpdate()
        }
      }
    

    Как мы видим, написанный парсер мы используем только в функции all. Обратите внимания, что для непосредственного соединения с базой данных используется DB.withConnection, а для создания запроса мы используем функцию Anorm SQL.

    Сейчас уже вы можете создавать дела. Попробуйте перейти на localhost.9000/tasks и создать несколько дел. Но ни удалять, ни делать завершенными вы дела еще не можете.


Теперь, когда мы «разделались» с моделью, можно заканчивать контроллер. Нам остались две функции: delete и complete. Про удаление — без комментариев, а про complete: у каждого дела в БД есть поле «ready». Если оно 0 — то дело незаконченно. В функции complete мы будем менять его на 1. Итак, приступим:
  def deleteTask(id: Long) = Action {
  	Task.delete(id)
  	Redirect(routes.Application.tasks)
  }

  def completeTask(id: Long) = Action {
        Task.complete(id)
        Redirect(routes.Application.tasks)
  }


Осталось пару штрихов. Немного доделаем фал app/views/index.scala.html.
Вот этот код:
@inputText(taskForm("label"))
@inputText(taskForm("who"))

заменим на:
@inputText(taskForm("label"), args = 'size -> 55, 'placeholder -> "Idea")
@inputText(taskForm("who"), args = 'placeholder -> "Your name")


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



Теперь перейдем к развертыванию приложения. Я буду использовать git и heroku. Но для начала надо создать Procfile в корне приложения (в папке todolist):
web: target/start -Dhttp.port=${PORT} -DapplyEvolutions.default=true -DapplyDownEvolutions.default=true -Ddb.default.url=${DATABASE_URL} -Ddb.default.driver=org.postgresql.Driver

Так же измените project/Build.scala:
import sbt._
import Keys._
import play.Project._

object ApplicationBuild extends Build {

  val appName         = "todolist"
  val appVersion      = "1.0-SNAPSHOT"

  val appDependencies = Seq(
    // Add your project dependencies here,
    jdbc,
    anorm,
    "postgresql" % "postgresql" % "8.4-702.jdbc4"
  )


  val main = play.Project(appName, appVersion, appDependencies).settings(
    // Add your own project settings here      
  )

}


Это надо сделать потому, что мы использовали базу данных H2, а на heroku база данных — PostgreSQL. Наконец-то мы можем перейти к непосредственному развертыванию (у вас должно быть установлено heroku, и вы должны быть зарегестрированы на сайте heroku.com), если нет:
$ wget -qO- https://toolbelt.heroku.com/install-ubuntu.sh | sh
$ heroku login

):
$ git init
$ git add .
$ git commit -m "init"
$ heroku create --stack cedar
$ git push heroku master
$ heroku open


Итоги



Подведем итоги. Сегодня мы познакомились с языком программирования Scala (не все, естественно), с фреймворком Play. Пользоваться им или нет — решать вам. Я на простом примере показал всю простоту их использования. Мне бы хотелось, чтобы каждый кто прочел эту статью начал пользоваться приобретенными знаниями и развивать их. Пишите на scala с play!



Полезные ссылки:

Play
Scala
Уроки по ФП на скале на coursera
Scala + Intellij Idea
Это приложение на github
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.