Pull to refresh

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
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.