Skip to content

Commit

Permalink
Merge pull request #18 from darkenpeng/db-sample
Browse files Browse the repository at this point in the history
feat : basic CRUD App using Doobie/tranzactio
  • Loading branch information
SHSongs authored Aug 8, 2023
2 parents d4bd029 + 82b5e6b commit 0607447
Show file tree
Hide file tree
Showing 25 changed files with 932 additions and 453 deletions.
355 changes: 197 additions & 158 deletions Forecast/src/main/scala/Forecast.scala

Large diffs are not rendered by default.

170 changes: 89 additions & 81 deletions bicycle_db/src/main/scala/BicycleRentalApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,91 +12,99 @@ import com.bicycle_db.RentalRecordServices
// Placeholders for the tables in the database
case class UsersTableRow(userId: String, password: String, balance: Int)
case class StationTableRow(stationId: Int, var availableBicycles: Int)
case class RentalRecordRow(userId: String, stationId: Int, endStation: Option[Int], rentalTime: Int, cost: Int)

case class RentalRecordRow(
userId: String,
stationId: Int,
endStation: Option[Int],
rentalTime: Int,
cost: Int
)

// ref: https://judo0179.tistory.com/96
object BicycleRentalApp extends ZIOAppDefault {

val prog = for {
_ <- ZIO.unit
database <- ZIO.service[Database]

// create services instances
userService = new UserServices(database)
stationService = new StationServices(database)
rentalRecordService = new RentalRecordServices(database)

// for testing purposes, delete all rows from the database
// _ <- userService.deleteAllUsers
// _ <- stationService.deleteAllStations
// _ <- rentalRecordService.deleteAllRentalRecords

rentalService = new BicycleRentalService(database)

//insert some data into the database
// _ <- userService.insertUserTableRow(UsersTableRow("foobar", "password1", 1000))
// _ <- stationService.insertStationTableRow(StationTableRow(123, 10)) // start station
// _ <- rentalRecordService.insertStationTableRow(StationTableRow(456, 10)) // end station

// login system
_ <- Console.printLine("Enter your user id: ")
userId <- Console.readLine
_ <- Console.printLine("Enter your password: ")
password <- Console.readLine

// check if the user is verified or not. if not, fail the program
isVerified <- rentalService.verifyUser(userId, password)
_ <- if (isVerified)
Console.printLine("Log in")
else
ZIO.fail("Can't find user")

// rent a bicycle
_ <- Console.printLine("Enter the station id: ")
stationId <- Console.readLine
isAvailable <- rentalService.checkBikeAvailability(stationId)
// if `isAvailable`, then get `rentTime` and proceed to rent a bicycle
// else, fail the program
_ <- if (isAvailable) {
for {
_ <- Console.printLine("Enter the rental time: ")
rentalTime <- Console.readLine
rentalCost = rentalService.calculateRentalCost(rentalTime.toInt)
_ <- rentalService.rentBike(userId, stationId.toInt, rentalTime.toInt)
_ <- Console.printLine(s"Your rental cost is $rentalCost")
} yield ()
} else {
ZIO.fail("No available bikes")
}

// return a bicycle
_ <- Console.printLine("Enter the station id: ")
returnStationId <- Console.readLine
_ <- rentalService.returnBike(userId, returnStationId.toInt)
_ <- Console.printLine("Bike has returned. Thank you for using our service!")
} yield ()

override def run = prog.provide(
conn >>> ConnectionSource.fromConnection >>> Database.fromConnectionSource
val prog = for {
_ <- ZIO.unit
database <- ZIO.service[Database]

// create services instances
userService = new UserServices(database)
stationService = new StationServices(database)
rentalRecordService = new RentalRecordServices(database)

// for testing purposes, delete all rows from the database
// _ <- userService.deleteAllUsers
// _ <- stationService.deleteAllStations
// _ <- rentalRecordService.deleteAllRentalRecords

rentalService = new BicycleRentalService(database)

//insert some data into the database
// _ <- userService.insertUserTableRow(UsersTableRow("foobar", "password1", 1000))
// _ <- stationService.insertStationTableRow(StationTableRow(123, 10)) // start station
// _ <- rentalRecordService.insertStationTableRow(StationTableRow(456, 10)) // end station

// login system
_ <- Console.printLine("Enter your user id: ")
userId <- Console.readLine
_ <- Console.printLine("Enter your password: ")
password <- Console.readLine

// check if the user is verified or not. if not, fail the program
isVerified <- rentalService.verifyUser(userId, password)
_ <-
if (isVerified)
Console.printLine("Log in")
else
ZIO.fail("Can't find user")

// rent a bicycle
_ <- Console.printLine("Enter the station id: ")
stationId <- Console.readLine
isAvailable <- rentalService.checkBikeAvailability(stationId)
// if `isAvailable`, then get `rentTime` and proceed to rent a bicycle
// else, fail the program
_ <-
if (isAvailable) {
for {
_ <- Console.printLine("Enter the rental time: ")
rentalTime <- Console.readLine
rentalCost = rentalService.calculateRentalCost(rentalTime.toInt)
_ <- rentalService.rentBike(userId, stationId.toInt, rentalTime.toInt)
_ <- Console.printLine(s"Your rental cost is $rentalCost")
} yield ()
} else {
ZIO.fail("No available bikes")
}

// return a bicycle
_ <- Console.printLine("Enter the station id: ")
returnStationId <- Console.readLine
_ <- rentalService.returnBike(userId, returnStationId.toInt)
_ <- Console.printLine(
"Bike has returned. Thank you for using our service!"
)

// docker run -p 5400:5400 --name bicycle -e POSTGRES_PASSWORD=<password> -d postgres
val postgres = locally {
val path = "localhost:5432"
val name = "rental_service"
val user = ???
val password = ???

s"jdbc:postgresql://$path/$name?user=$user&password=$password"
}

private val conn = ZLayer(
ZIO.attempt(
java.sql.DriverManager.getConnection(
postgres
)
)
} yield ()

override def run = prog.provide(
conn >>> ConnectionSource.fromConnection >>> Database.fromConnectionSource
)

// docker run -p 5400:5400 --name bicycle -e POSTGRES_PASSWORD=<password> -d postgres
val postgres = locally {
val path = "localhost:5432"
val name = "rental_service"
val user = ???
val password = ???

s"jdbc:postgresql://$path/$name?user=$user&password=$password"
}

private val conn = ZLayer(
ZIO.attempt(
java.sql.DriverManager.getConnection(
postgres
)
)
)
}

84 changes: 49 additions & 35 deletions bicycle_db/src/main/scala/BicycleRentalService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,66 @@ import cats.implicits._

class BicycleRentalService(db: Database) {

case class DatabaseError(message: String) extends Exception(message)
case class DatabaseError(message: String) extends Exception(message)

def fromSqlException: PartialFunction[Throwable, DatabaseError] = {
case e: java.sql.SQLException => DatabaseError(e.getMessage)
}

def rentBike(userId: String, stationId: Int, rentalTime: Int): ZIO[Any, Throwable, Int] = {
val rentalRecord = RentalRecord(userId, stationId, None, rentalTime, 1000)
// to prevent SQL injection, use doobie's Fragment API(`fr`) instead of string interpolation
val rentBicycleQuery = tzio {
(fr"UPDATE users SET balance = balance -" ++ fr"${rentalRecord.cost}" ++ fr"WHERE id =" ++ fr"$userId").update.run *>
(fr"INSERT INTO rentalRecord (userId, stationId, rentalTime, cost) VALUES (" ++ fr"$userId," ++ fr"$stationId," ++ fr"${rentalRecord.rentalTime}," ++ fr"${rentalRecord.cost}").update.run *>
(fr"UPDATE station SET availableBicycles = availableBikes - 1 WHERE stationId =" ++ fr"$stationId").update.run
}
def fromSqlException: PartialFunction[Throwable, DatabaseError] = {
case e: java.sql.SQLException => DatabaseError(e.getMessage)
}

db.transactionOrWiden(rentBicycleQuery).mapError(fromSqlException)
def rentBike(
userId: String,
stationId: Int,
rentalTime: Int
): ZIO[Any, Throwable, Int] = {
val rentalRecord = RentalRecord(userId, stationId, None, rentalTime, 1000)
// to prevent SQL injection, use doobie's Fragment API(`fr`) instead of string interpolation
val rentBicycleQuery = tzio {
(fr"UPDATE users SET balance = balance -" ++ fr"${rentalRecord.cost}" ++ fr"WHERE id =" ++ fr"$userId").update.run *>
(fr"INSERT INTO rentalRecord (userId, stationId, rentalTime, cost) VALUES (" ++ fr"$userId," ++ fr"$stationId," ++ fr"${rentalRecord.rentalTime}," ++ fr"${rentalRecord.cost}").update.run *>
(fr"UPDATE station SET availableBicycles = availableBikes - 1 WHERE stationId =" ++ fr"$stationId").update.run
}

def returnBike(userId: String, returnStationId: Int): ZIO[Any, Throwable, Int] = {
val returnBikeQuery = tzio {
(fr"UPDATE rentalRecord SET endStation =" ++ fr"$returnStationId" ++ fr"WHERE userId =" ++ fr"$userId" ++ fr"AND endStation IS NULL").update.run *>
(fr"UPDATE station SET availableBikes = availableBikes + 1 WHERE stationId =" ++ fr"$returnStationId").update.run
}
db.transactionOrWiden(rentBicycleQuery).mapError(fromSqlException)
}

db.transactionOrWiden(returnBikeQuery).mapError(fromSqlException)
def returnBike(
userId: String,
returnStationId: Int
): ZIO[Any, Throwable, Int] = {
val returnBikeQuery = tzio {
(fr"UPDATE rentalRecord SET endStation =" ++ fr"$returnStationId" ++ fr"WHERE userId =" ++ fr"$userId" ++ fr"AND endStation IS NULL").update.run *>
(fr"UPDATE station SET availableBikes = availableBikes + 1 WHERE stationId =" ++ fr"$returnStationId").update.run
}

def checkBikeAvailability(stationId: String): ZIO[Any, Throwable, Boolean] = {
val checkBikeAvailabilityQuery = tzio {
(fr"SELECT EXISTS (SELECT * FROM station WHERE stationId =" ++ fr"$stationId" ++ fr"AND availableBikes > 0)").query[Boolean].unique
}
db.transactionOrWiden(returnBikeQuery).mapError(fromSqlException)
}

db.transactionOrWiden(checkBikeAvailabilityQuery).mapError(fromSqlException)
def checkBikeAvailability(stationId: String): ZIO[Any, Throwable, Boolean] = {
val checkBikeAvailabilityQuery = tzio {
(fr"SELECT EXISTS (SELECT * FROM station WHERE stationId =" ++ fr"$stationId" ++ fr"AND availableBikes > 0)")
.query[Boolean]
.unique
}

def calculateRentalCost(rentalTime: Int): Int = {
rentalTime * 1000
}
db.transactionOrWiden(checkBikeAvailabilityQuery).mapError(fromSqlException)
}

//// Login System ////
def calculateRentalCost(rentalTime: Int): Int = {
rentalTime * 1000
}

def verifyUser(userId: String, password: String): ZIO[Any, Throwable, Boolean] = {
val verifyUserQuery = tzio {
(fr"SELECT EXISTS (SELECT * FROM users WHERE id =" ++ fr"$userId" ++ fr"AND password =" ++ fr"$password)").query[Boolean].unique
}
//// Login System ////

db.transactionOrWiden(verifyUserQuery).mapError(fromSqlException)
def verifyUser(
userId: String,
password: String
): ZIO[Any, Throwable, Boolean] = {
val verifyUserQuery = tzio {
(fr"SELECT EXISTS (SELECT * FROM users WHERE id =" ++ fr"$userId" ++ fr"AND password =" ++ fr"$password)")
.query[Boolean]
.unique
}
}

db.transactionOrWiden(verifyUserQuery).mapError(fromSqlException)
}
}
57 changes: 35 additions & 22 deletions bicycle_db/src/main/scala/RentalRecord.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,46 @@ import doobie.implicits._
import bicycle_db.RentalRecordRow
import zio.ZIO

case class RentalRecord(userId: String, stationId: Int, endStation: Option[Int], rentalTime: Int, cost: Int)
case class RentalRecord(
userId: String,
stationId: Int,
endStation: Option[Int],
rentalTime: Int,
cost: Int
)

class RentalRecordServices(db: Database) {
def insertRentalRecordRow(row: RentalRecordRow): ZIO[Database, Throwable, Int] = {
val insertRentalRecordQuery = tzio {
(fr"insert into rental_record (userId, stationId, endStation, rentalTime, cost) values (" ++ fr"${row.userId}," ++ fr"${row.stationId}," ++ fr"${row.endStation}," ++ fr"${row.rentalTime}," ++ fr"${row.cost})").update.run
}

db.transactionOrWiden(insertRentalRecordQuery)
def insertRentalRecordRow(
row: RentalRecordRow
): ZIO[Database, Throwable, Int] = {
val insertRentalRecordQuery = tzio {
(fr"insert into rental_record (userId, stationId, endStation, rentalTime, cost) values (" ++ fr"${row.userId}," ++ fr"${row.stationId}," ++ fr"${row.endStation}," ++ fr"${row.rentalTime}," ++ fr"${row.cost})").update.run
}

def fetchAndPrintRentalRecordData(db: Database): ZIO[Any, Throwable, Unit] = for {
rentalRecordInfo <- db.transactionOrWiden(for {
res <- tzio {
(fr"select userId, stationId, endStation, rentalTime, cost from rental_record limit 10").query[RentalRecordRow].to[List]
}
} yield res)

_ <- zio.Console.printLine(rentalRecordInfo)
} yield ()
db.transactionOrWiden(insertRentalRecordQuery)
}

def deleteAllRentalRecords: ZIO[Database, Throwable, Int] = {
val deleteAllRentalRecordsQuery = tzio {
(fr"delete from rental_record").update.run
def fetchAndPrintRentalRecordData(db: Database): ZIO[Any, Throwable, Unit] =
for {
rentalRecordInfo <- db.transactionOrWiden(for {
res <- tzio {
(fr"select userId, stationId, endStation, rentalTime, cost from rental_record limit 10")
.query[RentalRecordRow]
.to[List]
}
} yield res)

val db = ZIO.service[Database]
db.flatMap(database => database.transactionOrWiden(deleteAllRentalRecordsQuery))
_ <- zio.Console.printLine(rentalRecordInfo)
} yield ()

def deleteAllRentalRecords: ZIO[Database, Throwable, Int] = {
val deleteAllRentalRecordsQuery = tzio {
(fr"delete from rental_record").update.run
}
}

val db = ZIO.service[Database]
db.flatMap(database =>
database.transactionOrWiden(deleteAllRentalRecordsQuery)
)
}
}
Loading

0 comments on commit 0607447

Please sign in to comment.