REST API with Scala, Play, Slick, PostgreSQL, Redis and AWS S3

简介

我的第一个Play应用是一个REST服务.这是一些我在编写API时学到的东西.Play用于编写JSON接口,Slick用于和数据库交互,其中PostgreSQL作为主数据库,Redis用于用户系统/统计数据和缓存,AWS S3用于存储文件,比如图片和视频音频.

项目结构

app
  -- controllers
    -- controllers go here
  -- helpers
    -- helper classes
  -- models
    -- Tables.scala             // this contains all the table definitions
    -- UserModel.scala
  -- views
    -- I dont use this directory as its a JSON API
  -- Global.scala               // this contains code that starts the akka actor system that Redis library uses
conf
  -- application.conf
  -- routes

数据结构

在app/models/Tables.scala中定义基本的数据库表结构,由于使用Slick和PostgreSQL进行交互,需要定义数据结构对PostgreSQL中的表结构进行映射.

Tables.scala有两个顶层对象,包含了和postgres表一一对应的case类的Types(Slick用于类型检查),和包含所有表结构定义的Tables.

一共有四个表:User, UserAuth, Message, UserConnection,下面是Types的定义:

import org.joda.time._          // this gives me DateTime

object Types {
  case class User(id:Option[Int]=None, name: String, gender: Option[Int]=None, dob: Option[DateTime]=None, profilePic: Option[String]=None, createdAt: DateTime, status: Int)

  case class UserAuth(userId: Int, `type`: Int, key: String, secret: String)   // type is a keyword in scala so to tell scala compiler to not interpret it the keyword type, i sorround it by backticks

  case class UserConnection(fromUserId: Int, toUserId: Int, connectedAt: DateTime)      // directed graph, used for follower/following relationship

  case class Message(id:Option[Int]=None, fromUserId: Int, toUserId: Int, sentAt: DateTime, text: Option[String]=None)
}

上面的每个case类中,id字段的类型均为Option[Int],并且给出默认值为None,因为在往数据库插入一行数据时并不能直到这行数据的id值,因此当插入时并不需要提供该字段的值,而是由postgres自动提供.

case类User中gender的类型为Option[Int],同时提供默认值为None,因为用户可能并不会提供性别,因此在数据表中定义为null,字段dob和profilePic也是一样道理.

下面是数据表定义:

import scala.slick.ast.ColumnOption.DBType                  // need this to use data types like varchar and text
import scala.slick.driver.PostgresDriver.simple._           // need to interact with postgres
import com.github.tototoshi.slick.PostgresJodaSupport._     // need for use postgres's datetime types

object Tables {
  class User(tag: Tag) extends Table[Types.User](tag, "users") {
    def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
    def name = column[String]("name")
    def gender = column[Option[Int]]("gender")
    def dob = column[Option[DateTime]]("dob")
    def profilePic = column[Option[String]]("profile_pic")
    def createdAt = column[DateTime]("created_at")
    def status = column[Int]("status", O.Default(1))        // status has defaul vlaue of 1
    def * = (id.?,name,gender,dob,profilePic,createdAt,status) <> (Types.User.tupled, Types.User.unapply)
  }

  class UserAuth(tag: Tag) extends Table[Types.UserAuth](tag, "user_auth") {
    def userId = column[Int]("user_id")
    def `type` = column[Int]("type")
    def key = column[String]("key")
    def secret = column[String]("secret")
    def * = (userId,`type`,key,secret) <> (Types.UserAuth.tupled, Types.UserAuth.unapply)

    def user = foreignKey("USER_FK", userId, TableQuery[User])(_.id)
    def idx = index("USER_AUTH", (userId, `type`), unique = true)
  }

  class UserConnection(tag: Tag) extends Table[Types.UserConnection](tag, "user_connections") {
    def fromUserId = column[Int]("from_user_id")
    def toUserId = column[Int]("to_user_id")
    def connectedAt = column[DateTime]("connected_at")
    def * = (fromUserId,toUserId,connectedAt) <> (Types.UserConnection.tupled, Types.UserConnection.unapply)
  }

  class Message(tag: Tag) extends Table[Types.Message](tag, "messages") {
    def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
    def fromUserId = column[Int]("from_user_id")
    def toUserId = column[Int]("to_user_id")
    def sentAt = column[DateTime]("sent_at")
    def text = column[String]("text", DBType("text"))
    def * = (id.?,fromUserId,toUserId,sentAt,text) <> (Types.Message.tupled, Types.Message.unapply)
  }
}

“class User(tag: Tag) extends Table[Types.User](tag, “users”)”告诉Scala下面的表结构定义对应于case类Types.User,同时表名为”users”.

“def id = column[Int](“id”, O.PrimaryKey, O.AutoInc)”表示字段”id”为该表的自增主键ID.

“def gender = column[Option[Int]](“gender”)”表示字段gender是Int类型,并且可能为空.

“def * = (id.?,name,gender,dob,profilePic,createdAt,status) <> (Types.User.tupled, Types.User.unapply)”告诉Slick,任何从该表查询到的行,都返回括号中的列,同时提供数据表到case类的交互转换(参考详解).

“def user = foreignKey(“USERFK”, userId, TableQuery[User])(.id)”,该方法定义了一个外键,即UserAuth表中的userId字段引用User表中的id字段.

由于每个用户对应不同类型的网站(FB,google,Twitter)都有一个唯一的认证,以此通过”def idx = index(“USER_AUTH”, (userId, `type`), unique = true)”方法创建一个符合唯一索引.

如果想要使用postgres中的字段类型时,比如varchar,则可以按这样的方式:”def text = column[String](“text”, DBType(“text”))”,使用DBType进行声明,甚至声明字段长度:”def text = column[String](“text”, DBType(“varchar(100)”))”.

参考

参考连接