From 0a89c57d009c30a9081e13b2cd69491ff4a05c05 Mon Sep 17 00:00:00 2001 From: augustnagro Date: Sun, 1 Dec 2024 16:17:13 -0800 Subject: [PATCH] Spec changes --- README.md | 93 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index a98a97a..24b5aa0 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Yet another database client for Scala. No dependencies, high productivity. ## Installing ``` -"com.augustnagro" %% "magnum" % "1.3.0" +"com.augustnagro" %% "magnum" % "2.0.0" ``` Magnum requires Scala >= 3.3.0 @@ -218,7 +218,7 @@ The optional `@Id` annotation denotes the table's primary key. Not setting `@Id` It is a best practice to extend ImmutableRepo to encapsulate your SQL in repositories. This way, it's easier to maintain since they're grouped together. ```scala -class UserRepo extends ImmutableRepo[User, Long]: +object UserRepo extends ImmutableRepo[User, Long]: def firstNamesForLast(lastName: String)(using DbCon): Vector[String] = sql""" SELECT DISTINCT first_name @@ -229,18 +229,22 @@ class UserRepo extends ImmutableRepo[User, Long]: // other User-related queries here ``` +If you don't want to expose all the ImmutableRepo methods, use a Scala 3 [exports clause](https://docs.scala-lang.org/scala3/reference/other-new-features/export.html): + +```scala +object UserRepo: + private val repo = ImmutableRepo[User, Long] + + export repo.{count, existsById} + + // other User-related queries here +``` + ### Repositories -The `Repo` class auto-generates the following methods at compile-time: +The `Repo` class extends ImmutableRepo and also defines: ```scala - def count(using DbCon): Long - def existsById(id: ID)(using DbCon): Boolean - def findAll(using DbCon): Vector[E] - def findAll(spec: Spec[E])(using DbCon): Vector[E] - def findById(id: ID)(using DbCon): Option[E] - def findAllById(ids: Iterable[ID])(using DbCon): Vector[E] - def delete(entity: E)(using DbCon): Unit def deleteById(id: ID)(using DbCon): Unit def truncate()(using DbCon): Unit @@ -275,7 +279,7 @@ val countAfterUpdate = transact(xa): It is a best practice to encapsulate your SQL in repositories. ```scala -class UserRepo extends Repo[User, User, Long] +object UserRepo extends Repo[User, User, Long] ``` Also note that Repo extends ImmutableRepo. Some databases cannot support every method, and will throw UnsupportedOperationException. @@ -315,18 +319,9 @@ val newUser: User = transact(xa): Specifications help you write safe, dynamic queries. An example use-case would be a search results page that allows users to sort and filter the paginated data. -1. If you need to perform joins to get the data needed, first create a database view. -2. Next, create an entity class that derives DbCodec. -3. Finally, use the Spec class to create a specification. - Here's an example: ```scala -val partialName = "Ja" -val lastNameOpt = Option("Brown") -val searchDate = OffsetDateTime.now.minusDays(2) -val idPosition = 42L - val spec = Spec[User] .where(sql"first_name ILIKE '$partialName%'") .where(lastNameOpt.map(ln => sql"last_name = $ln").getOrElse(sql"")) @@ -335,9 +330,14 @@ val spec = Spec[User] .limit(10) val users: Vector[User] = userRepo.findAll(spec) + +def partialName = "Ja" +def lastNameOpt = Option("Brown") +def searchDate = OffsetDateTime.now.minusDays(2) +def idPosition = 42L ``` -Note that both [seek pagination](https://blog.jooq.org/faster-sql-paging-with-jooq-using-the-seek-method/) and offset pagination is supported. +Note that both [seek pagination](https://blog.jooq.org/faster-sql-paging-with-jooq-using-the-seek-method/) and offset pagination is supported. If you need to use joins to select the columns, use the `Spec.prefix` method. ### Scala 3 Enum & NewType Support @@ -403,7 +403,7 @@ A common problem when writing SQL queries is that they're difficult to refactor. There's also lots of repetition when writing SQL. Magnum's repositories help scrap the boilerplate, but writing `SELECT a, b, c, d, ...` for a large table quickly gets tiring. -To help with this, Magnum offers a `TableInfo` class to enable 'future-proof' queries. An important caveat is that these queries are harder to copy/paste into SQL editors like PgAdmin or DbBeaver. +To help with this, Magnum offers a `TableInfo` class to enable 'future-proof' queries. An important caveat is that these queries are harder to copy/paste into SQL editors like PgAdmin or DbBeaver (of course, you can still find them in [DEBUG logs](#logging-sql-queries)) Here's some examples: @@ -415,29 +415,26 @@ case class UserCreator(firstName: String, age: Int) derives DbCodec @Table(PostgresDbType, SqlNameMapper.CamelToSnakeCase) case class User(id: Long, firstName: String, age: Int) derives DbCodec -object User: - val Table = TableInfo[UserCreator, User, Long] +object UserSql: + private val u = TableInfo[UserCreator, User, Long] -def allUsers(using DbCon): Vector[User] = - val u = User.Table - // equiv to - // SELECT id, first_name, age FROM user - sql"SELECT ${u.all} FROM $u".query[User].run() + def allUsers(using DbCon): Vector[User] = + // equiv to + // SELECT id, first_name, age FROM user + sql"SELECT ${u.all} FROM $u".query[User].run() -def firstNamesForLast(lastName: String)(using DbCon): Vector[String] = - val u = User.Table - // equiv to - // SELECT DISTINCT first_name FROM user WHERE last_name = ? - sql""" - SELECT DISTINCT ${u.firstName} FROM $u - WHERE ${u.lastName} = $lastName - """.query[String].run() + def firstNamesForLast(lastName: String)(using DbCon): Vector[String] = + // equiv to + // SELECT DISTINCT first_name FROM user WHERE last_name = ? + sql""" + SELECT DISTINCT ${u.firstName} FROM $u + WHERE ${u.lastName} = $lastName + """.query[String].run() -def insertOrIgnore(creator: UserCreator)(using DbCon): Unit = - val u = User.Table - // equiv to - // INSERT OR IGNORE INTO user (first_name, age) VALUES (?, ?) - sql"INSERT OR IGNORE INTO $u ${u.insertCols} VALUES ($creator)".update.run() + def insertOrIgnore(creator: UserCreator)(using DbCon): Unit = + // equiv to + // INSERT OR IGNORE INTO user (first_name, age) VALUES (?, ?) + sql"INSERT OR IGNORE INTO $u ${u.insertCols} VALUES ($creator)".update.run() ``` It's important that `val Table = TableInfo[X, Y, Z]` is not explicitly typed, otherwise its structural typing will be destroyed. @@ -562,6 +559,16 @@ case class Address( zipCode: String, country: String ) derives DbCodec + +def companyInfo(companyName: String)(using DbCon): Vector[(Company, Address)] = + val c = TableInfo[Company, Company, String].alias("c") + val a = TableInfo[Address, Address, Long].alias("a") + sql""" + SELECT ${c.all}, ${a.all}" + FROM $c + JOIN $a ON ${a.id} = ${c.addressId} + WHERE ${c.name} = $companyName + """.query[(Company, Address)].run() ``` #### UUID DbCodec doesn't work for my database @@ -580,7 +587,5 @@ case class Person(@Id id: Long, name: String, tracking_id: Option[UUID]) derives ``` ## Todo -* JSON / XML support * Support MSSql * Cats Effect & ZIO modules -* Explicit Nulls support