- Постоянство: мы используем scalaquery для сохранения элементов нашей модели.
- Безопасность: обрабатывать заголовок безопасности, содержащий ключ API.
  Фрист, мы посмотрим на постоянство части.  Для этой части мы будем использовать scalaquery .  Обратите внимание, что код, который мы здесь показываем, в значительной степени такой же, как и для преемника scalaquery.  Slick , однако, требует Scala 2.10.0-M7, и это будет означать, что мы должны изменить нашу полную настройку Scala.  Так что для этого примера мы будем просто использовать scalaquery (чей синтаксис такой же, как у slick).  Если вы еще этого не сделали, установите JRebel, чтобы ваши изменения были отражены мгновенно без перезапуска службы. 
 
  живучесть 
Я использовал postgresql для этого примера, но можно использовать любую из баз данных, поддерживаемых scalaquery. Модель базы данных, которую я использовал, очень проста:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | CREATE TABLE sc_bid(  id integer NOT NULL DEFAULT nextval('sc_bid_id_seq1'::regclass),  'for'integer,  min numeric,  max numeric,  currency text,  bidder integer,  date numeric,  CONSTRAINT sc_bid_pkey1PRIMARY KEY (id ),  CONSTRAINT sc_bid_bidder_fkey FOREIGN KEY (bidder)      REFERENCES sc_user (id) MATCH SIMPLE      ON UPDATE NO ACTION ON DELETE NO ACTION,  CONSTRAINT sc_bid_for_fkey FOREIGN KEY ('for')      REFERENCES sc_item (id) MATCH SIMPLE      ON UPDATE NO ACTION ON DELETE NO ACTION) CREATE TABLE sc_item(  id integer NOT NULL DEFAULT nextval('sc_bid_id_seq'::regclass),  name text,  price numeric,  currency text,  description text,  owner integer,  CONSTRAINT sc_bid_pkey PRIMARY KEY (id ),  CONSTRAINT sc_bid_owner_fkey FOREIGN KEY (owner)      REFERENCES sc_user (id) MATCH SIMPLE      ON UPDATE NO ACTION ON DELETE NO ACTION) CREATE TABLE sc_user(  id serial NOT NULL,  username text,  firstname text,  lastname text,  CONSTRAINT sc_user_pkey PRIMARY KEY (id )) | 
Как вы можете простую модель, с парой внешних ключей и первичных ключей, которые генерируются автоматически. Мы определяем таблицу для пользователей, для товаров и предложений. Обратите внимание, что это зависит от базы данных, так что это будет работать только для postgresql. Дополнительная заметка о postgresql и scalaquery. Scalaquery не поддерживает схемы. Это означает, что мы должны определить таблицы в «публичной» схеме.
Прежде чем мы сможем начать работу с scalaquery, мы должны сначала добавить его в наш проект. В build.sbt добавьте следующие зависимости
| 1 2 | 'org.scalaquery'%%'scalaquery'%'0.10.0-M1','postgresql'%'postgresql'%'9.1-901.jdbc4' | 
После обновления у вас будут необходимые фляги для scalaquery и postgres. Давайте посмотрим на один из репозиториев: bidrepository и признак RepositoryBase.
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | // the traitimportorg.scalaquery.session.Database traitRepositoryBase {  valdb =Database.forURL('jdbc:postgresql://localhost/dutch_gis?user=jos&password=secret', driver ='org.postgresql.Driver')} // simple implementation of the bidrepositorypackageorg.smartjava.scalatra.repositoryimportorg.smartjava.scalatra.model.Bidimportorg.scalaquery.session._importorg.scalaquery.ql.basic.{BasicTable => Table}importorg.scalaquery.ql.TypeMapper._importorg.scalaquery.ql._importorg.scalaquery.ql.extended.PostgresDriver.Implicit._importorg.scalaquery.session.Database.threadLocalSession classBidRepository extendsRepositoryBase {   objectBidMapping extendsTable[(Option[Long], Long, Double, Double, String, Long, Long)]('sc_bid') {      defid =column[Option[Long]]('id', O PrimaryKey)      defforItem =column[Long]('for', O NotNull)      defmin =column[Double]('min', O NotNull)      defmax =column[Double]('max', O NotNull)      defcurrency =column[String]('currency')      defbidder =column[Long]('bidder', O NotNull)      defdate =column[Long]('date', O NotNull)       defnoID =forItem ~ min ~ max ~ currency ~ bidder ~ date      def* =id ~ forItem ~ min ~ max ~ currency ~ bidder ~ date  }   /**   * Return a Option[Bid] if found or None otherwise   */  defget(bid:Long, user:String) :Option[Bid] ={      varresult:Option[Bid] =None;       db withSession {          // define the query and what we want as result       valquery =for(u <-BidMapping ifu.id ===bid) yieldu.id ~ u.forItem ~ u.min ~ u.max ~ u.currency ~ u.bidder ~ u.date        // map the results to a Bid object       valinter =query mapResult {         case(id,forItem,min,max,currency,bidder,date) => Option(newBid(id,forItem, min, max, currency, bidder, date));       }        // check if there is one in the list and return it, or None otherwise       result =inter.list match{         case_::tail => inter.first         caseNil => None       }      }       // return the found bid      result    }     /**     * Create a bid using scala query. This will always create a new bid     */    defcreate(bid:Bid):Bid ={      varid:Long =-1;       // start a db session      db withSession {        // create a new bid        valres =BidMapping.noID insert (bid.forItem.longValue, bid.minimum.doubleValue, bid.maximum.doubleValue, bid.currency, bid.bidder.toLong, System.currentTimeMillis());        // get the autogenerated bid        validQuery =Query(SimpleFunction.nullary[Long]('LASTVAL'));        id =idQuery.list().head;      }      // create a bid to return      valcreatedBid =newBid(Option(id), bid.forItem, bid.minimum, bid.maximum, bid.currency, bid.bidder, bid.date);      createdBid;    }     /**     * Delete a bid     */    defdelete(user:String, bid:Long) :Option[Bid] ={      // get the bid we're deleting      valresult =get(bid,user);       // delete the bid      valtoDelete =BidMapping where (_.id ===bid)      db withSession {        toDelete.delete      }       // return deleted bid      result    }} | 
Выглядит сложно, верно? Мы сделаем это не раз, когда вы поймете, как работает скаляр. С помощью scalaquery вы создаете отображение таблицы. В этом отображении вы указываете тип полей, которые вы ожидаете. В этом примере наша таблица сопоставления выглядит следующим образом:
| 01 02 03 04 05 06 07 08 09 10 11 12 | objectBidMapping extendsTable[(Option[Long], Long, Double, Double, String, Long, Long)]('sc_bid') {    defid =column[Option[Long]]('id', O PrimaryKey)    defforItem =column[Long]('for', O NotNull)    defmin =column[Double]('min', O NotNull)    defmax =column[Double]('max', O NotNull)    defcurrency =column[String]('currency')    defbidder =column[Long]('bidder', O NotNull)    defdate =column[Long]('date', O NotNull)    defnoID =forItem ~ min ~ max ~ currency ~ bidder ~ date    def* =id ~ forItem ~ min ~ max ~ currency ~ bidder ~ date} | 
 Здесь мы определяем отображение таблицы ‘sc_bid’.  Для каждого поля мы определяем имя столбца и его тип.  Если мы хотим, мы можем добавить конкретные параметры, которые учитываются при создании вашего ddl из этого (не то, что я использовал для этого примера).  Последние два определения определяют «конструкторы» для этого отображения.  ‘Def *’ — это конструктор по умолчанию, где у нас есть все поля заранее, а ‘def noID’ — это тот, который мы будем использовать, когда создадим ставку в первый раз, и у нас пока нет идентификатора.  Помните, что идентификаторы автоматически генерируются базой данных. 
  С этим отображением мы можем начать писать наши функции репозитория.  Начнем с первого: получить 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /** * Return a Option[Bid] if found or None otherwise */defget(bid:Long, user:String) :Option[Bid] ={    varresult:Option[Bid] =None;    db withSession {        // define the query and what we want as result     valquery =for(u <-BidMapping ifu.id ===bid) yieldu.id ~ u.forItem ~ u.min ~ u.max ~ u.currency ~ u.bidder ~ u.date     // map the results to a Bid object     valinter =query mapResult {       case(id,forItem,min,max,currency,bidder,date) => Option(newBid(id,forItem, min, max, currency, bidder, date));     }     // check if there is one in the list and return it, or None otherwise     result =inter.list match{       case_::tail => inter.first       caseNil => None     }    }    // return the found bid    result  } | 
Здесь вы можете видеть, что мы используем стандартную конструкцию scala для создания итерации запроса по таблице, отображенной с помощью BidMapping. Чтобы убедиться, что мы получаем только то поле, которое нам нужно, мы применяем фильтр, используя оператор «if u.id === bid». В выражении yield мы указываем поля, которые мы хотим вернуть. Используя mapResult в запросе, мы можем обработать результаты запроса и преобразовать его в наш объект case и добавить его в список. Затем мы проверяем, действительно ли что-то есть в списке и возвращаем Option [Bid]. Обратите внимание, что это может быть написано более кратко, но это хорошо объясняет шаги, которые вы должны предпринять.
Следующая функция создать
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | defcreate(bid:Bid):Bid ={      varid:Long =-1;       // start a db session      db withSession {        // create a new bid        valres =BidMapping.noID insert (bid.forItem.longValue, bid.minimum.doubleValue, bid.maximum.doubleValue, bid.currency, bid.bidder.toLong, System.currentTimeMillis());        // get the autogenerated bid        validQuery =Query(SimpleFunction.nullary[Long]('LASTVAL'));        id =idQuery.list().head;      }      // create a bid to return      valcreatedBid =newBid(Option(id), bid.forItem, bid.minimum, bid.maximum, bid.currency, bid.bidder, bid.date);      createdBid;    } | 
Теперь мы используем пользовательский noid BidMapping для конструктора, чтобы сгенерировать оператор вставки. Если мы не указали noID, мы должны уже указать id. Теперь, когда мы вставили новый объект Bid в базу данных, нам нужно вернуть только что созданный Bid с новым идентификатором пользователю. Для этого нам нужно выполнить простой запрос LASTVAL, который возвращает последнее автоматически сгенерированное значение. В нашем случае это идентификатор созданной ставки. Из этой информации мы создаем новую ставку, которую мы возвращаем.
Последняя операция для нашего репозитория — это функция удаления. Эта функция сначала проверяет наличие указанной ставки и, если она есть, удаляет ее.
| 01 02 03 04 05 06 07 08 09 10 11 12 13 | defdelete(user:String, bid:Long) :Option[Bid] ={  // get the bid we're deleting  valresult =get(bid,user);  // delete the bid  valtoDelete =BidMapping where (_.id ===bid)  db withSession {    toDelete.delete  }  // return deleted bid  result} | 
Здесь мы используем фильтр ‘where’ для создания запроса, который мы хотим выполнить. Когда мы вызываем delete в этом фильтре, все соответствующие элементы удаляются. И это самое простое использование скалябства для настойчивости. Если вам нужны более сложные операции (например, объединения), посмотрите примеры на сайте scalaquery.org.
  Теперь у нас есть функциональность для создания и удаления ставок.  Поэтому было бы неплохо, если бы у нас был какой-то способ аутентификации наших пользователей.  Для этого урока мы собираемся создать очень простую схему аутентификации на основе API-ключей.  Для каждого запроса пользователь должен добавить определенный заголовок со своим ключом API.  Затем мы можем использовать информацию из этого ключа, чтобы определить, кто этот пользователь, и может ли он удалить или получить доступ к конкретной информации. 
 
  Безопасность 
Начнем с части генерации ключей. Когда кто-то хочет использовать наш API, мы требуем от него указать имя приложения и имя хоста, с которого будет сделан запрос. Эту информацию мы будем использовать для генерации ключа, который они должны использовать в каждом запросе. Этот ключ — простой хэш HMAC.
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | packageorg.smartjava.scalatra.util importjavax.crypto.spec.SecretKeySpecimportjavax.crypto.Macimportorg.apache.commons.codec.binary.Base64 objectSecurityUtil {   defcalculateHMAC(secret:String, applicationName:String , hostname:String ) :String  ={    valsigningKey =newSecretKeySpec(secret.getBytes(),'HmacSHA1');    valmac =Mac.getInstance('HmacSHA1');    mac.init(signingKey);    valrawHmac =mac.doFinal((applicationName + '|'+ hostname).getBytes());     newString(Base64.encodeBase64(rawHmac));  }   defcheckHMAC(secret:String, applicationName:String, hostname:String, hmac:String) :Boolean  ={    returncalculateHMAC(secret, applicationName, hostname) ==hmac;  }   defmain(args:Array[String]) {    valhmac =SecurityUtil.calculateHMAC('The passphrase to calculate the secret with','App 1','localhost');    println(hmac);    println(SecurityUtil.checkHMAC('The passphrase to calculate the secret with','App 1','localhost',hmac));  }} | 
Приведенный выше вспомогательный объект используется для вычисления начального хэша, который мы отправляем пользователю, и может использоваться для проверки входящего хэша. Чтобы использовать это в нашем REST API, нам нужно перехватить все входящие запросы и проверить эти заголовки перед вызовом определенного маршрута. С помощью scalatra мы можем сделать это с помощью функции before ():
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | packageorg.smartjava.scalatra.routesimportorg.scalatra.ScalatraBaseimportorg.smartjava.scalatra.repository.KeyRepository /** * When this trait is used, the incoming request * is checked for authentication based on the * X-API-Key header. */traitAuthentication extendsScalatraBase {   valApiHeader ='X-API-Key';  valAppHeader ='X-API-Application';  valKeyChecker =newKeyRepository;    /**   * A simple interceptor that checks for the existence    * of the correct headers   */  before() {    // we check the host where the request is made    valservername =request.serverName;    valheader =Option(request.getHeader(ApiHeader));    valapp =Option(request.getHeader(AppHeader));     List(header,app) match{      caseList(Some(x),Some(y)) => isValidHost(servername,x,y);      case_=> halt(status=401, headers=Map('WWW-Authenticate'-> 'API-Key'));    }  }   /**   * Check whether the host is valid. This is done by checking the host against   * a database with keys.   */  privatedefisValidHost(hostName:String, apiKey:String, appName:String):Boolean ={    KeyChecker.validateKey(apiKey, appName, hostName);  } } | 
Эта черта, которую мы включили в наш основной сервлет скалатры, получает правильную информацию из запроса и проверяет, соответствует ли предоставленный хеш сгенерированным кодом, который вы видели ранее. Если это так, запрос передается, если нет, мы останавливаем обработку запроса и отправляем обратно 401, объясняющий, как пройти аутентификацию с помощью этого API.
Если клиент пропустит эти заголовки, он получит это в ответ:
Если клиент отправит правильные заголовки, он получит такой ответ:
Вот именно для этой части. В следующей части мы рассмотрим внедрение Indency, CQRS, Akka и запуск этого кода в облаке.
Ссылка: Учебное пособие: Начало работы со scala и scalatra — часть III от нашего партнера JCG Йоса Дирксена из блога Smart Java .

