diff --git a/.env.sample b/.env.sample index 6a2dedba..0ea2cf09 100644 --- a/.env.sample +++ b/.env.sample @@ -1,5 +1,5 @@ -DB_JDBC_URL=jdbc:p6spy:mysql://localhost:3306/aisu?createDatabaseIfNotExist=true&connectionTimeZone=UTC -DB_USERNAME=root +DB_JDBC_URL=jdbc:p6spy:postgresql://localhost:5432/aisu +DB_USERNAME=aisu DB_PASSWORD=4ab4008b362142c391cc9e1ab98addc6b25f00f5 REDIS_CONNECTION_URL=redis://localhost:6379 APP_ENV=local diff --git a/.env.testing b/.env.testing index c35dbe52..5cb4f407 100644 --- a/.env.testing +++ b/.env.testing @@ -1,5 +1,5 @@ -DB_JDBC_URL=jdbc:mysql://localhost:3306/aisu_test?createDatabaseIfNotExist=true -DB_USERNAME=root +DB_JDBC_URL=jdbc:postgresql://localhost:5432/aisu_test +DB_USERNAME=aisu DB_PASSWORD=4ab4008b362142c391cc9e1ab98addc6b25f00f5 # h2で動くようになったらコレに変える diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f1987ccc..835172ef 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,16 +15,15 @@ jobs: runs-on: ubuntu-latest services: db: - image: mariadb:10.11.5 + image: postgres:16.0 ports: - - 3306:3306 + - 5432:5432 env: - MYSQL_ROOT_PASSWORD: 4ab4008b362142c391cc9e1ab98addc6b25f00f5 - MYSQL_DATABASE: aisu - MYSQL_USER: aisu - MYSQL_PASSWORD: 4ab4008b362142c391cc9e1ab98addc6b25f00f5 + POSTGRES_DB: aisu_test + POSTGRES_USER: aisu + POSTGRES_PASSWORD: 4ab4008b362142c391cc9e1ab98addc6b25f00f5 options: >- - --health-cmd "mysqladmin ping" + --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 diff --git a/README.md b/README.md index 25fbfb3a..013fb973 100644 --- a/README.md +++ b/README.md @@ -3,5 +3,5 @@ aisu is a cheki management application backend made with Kotlin. ## Requirements - Java 19 or newer -- MySQL 8.0.29 or newer(MariaDB 10.7.3 or newer) +- PostgreSQL 15 or newer - Redis 7.0.0 or newer diff --git a/build.gradle.kts b/build.gradle.kts index 57bb228a..d419545c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -89,6 +89,7 @@ dependencies { implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion") implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion") implementation("mysql:mysql-connector-java:8.0.33") + implementation("org.postgresql:postgresql:42.6.0") implementation("com.zaxxer:HikariCP:5.0.1") implementation("io.insert-koin:koin-core:$koinVersion") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.1") diff --git a/docker-compose.yml b/docker-compose.yml index bdfeee7f..1f3aa686 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,15 @@ version: "3.9" services: - mysql: - image: mariadb:10.11.5 + postgres: + image: postgres:16.0 ports: - - "3306:3306" + - "5432:5432" volumes: - - mysql_data:/var/lib/mysql + - postgres_data:/var/lib/postgresql/data environment: - - MYSQL_DATABASE=aisu - - MYSQL_ROOT_PASSWORD=4ab4008b362142c391cc9e1ab98addc6b25f00f5 - - MYSQL_USER=aisu - - MYSQL_PASSWORD=4ab4008b362142c391cc9e1ab98addc6b25f00f5 + - POSTGRES_DB=aisu + - POSTGRES_USER=aisu + - POSTGRES_PASSWORD=4ab4008b362142c391cc9e1ab98addc6b25f00f5 redis: image: redis:7.2.1 ports: @@ -26,5 +25,5 @@ services: environment: SWAGGER_JSON: /oas3/openapi.yaml volumes: - mysql_data: + postgres_data: redis_data: diff --git a/src/main/kotlin/com/tumugin/aisu/infra/repository/exposed/DataTimeWithTimeZoneColumnType.kt b/src/main/kotlin/com/tumugin/aisu/infra/repository/exposed/DataTimeWithTimeZoneColumnType.kt index 7963b63a..a287f774 100644 --- a/src/main/kotlin/com/tumugin/aisu/infra/repository/exposed/DataTimeWithTimeZoneColumnType.kt +++ b/src/main/kotlin/com/tumugin/aisu/infra/repository/exposed/DataTimeWithTimeZoneColumnType.kt @@ -3,6 +3,9 @@ package com.tumugin.aisu.infra.repository.exposed import kotlinx.datetime.* import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.vendors.* +import java.sql.ResultSet +import java.time.OffsetDateTime +import java.time.ZoneOffset import java.time.format.DateTimeFormatter class DataTimeWithTimeZoneColumnType : ColumnType(), IDateColumnType { @@ -19,11 +22,8 @@ class DataTimeWithTimeZoneColumnType : ColumnType(), IDateColumnType { return when (value) { is Instant -> value is java.time.Instant -> value.toKotlinInstant() - // タイムゾーン情報を付けられないMySQLのようなDBの場合には強制的にタイムゾーンをUTCとして扱う - is LocalDateTime -> value.toInstant(UtcOffset.ZERO) - is java.time.LocalDateTime -> value.toKotlinLocalDateTime().toInstant(UtcOffset.ZERO) - is java.sql.Timestamp -> value.toLocalDateTime().toKotlinLocalDateTime().toInstant(UtcOffset.ZERO) - else -> error("$value is not Instant or LocalDateTime!") + is OffsetDateTime -> value.toInstant().toKotlinInstant() + else -> error("$value is not Instant or OffsetDateTime!") } } @@ -35,7 +35,13 @@ class DataTimeWithTimeZoneColumnType : ColumnType(), IDateColumnType { // タイムゾーン情報を付けられないMySQLのようなDBの場合には強制的にタイムゾーンをUTCとして扱う return value.toLocalDateTime(TimeZone.UTC).toJavaLocalDateTime() } - return value.toJavaInstant() + + // see here for pgsql: https://jdbc.postgresql.org/documentation/query/#table51-supportedjava-8-date-and-time-classes + return value.toJavaInstant().atOffset(ZoneOffset.UTC) + } + + override fun readObject(rs: ResultSet, index: Int): Any? { + return rs.getObject(index, OffsetDateTime::class.java) } private fun currentDBSupportsTimeZone(): Boolean { diff --git a/src/main/kotlin/com/tumugin/aisu/infra/repository/exposed/DateFormatWithTZFunction.kt b/src/main/kotlin/com/tumugin/aisu/infra/repository/exposed/DateFormatWithTZFunction.kt index 5086a6c4..32db56c5 100644 --- a/src/main/kotlin/com/tumugin/aisu/infra/repository/exposed/DateFormatWithTZFunction.kt +++ b/src/main/kotlin/com/tumugin/aisu/infra/repository/exposed/DateFormatWithTZFunction.kt @@ -8,12 +8,36 @@ import org.jetbrains.exposed.sql.CharColumnType import org.jetbrains.exposed.sql.ExpressionWithColumnType import org.jetbrains.exposed.sql.QueryBuilder import org.jetbrains.exposed.sql.append +import org.jetbrains.exposed.sql.vendors.MysqlDialect +import org.jetbrains.exposed.sql.vendors.PostgreSQLDialect +import org.jetbrains.exposed.sql.vendors.currentDialect class DateFormatWithTZFunction>( private val exp: T, private val format: String, private val fromTZ: TimeZone, private val toTZ: TimeZone ) : org.jetbrains.exposed.sql.Function(CharColumnType()) { override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { - append("DATE_FORMAT(CONVERT_TZ(", exp, ", '${toOffsetString(fromTZ)}', '${toOffsetString(toTZ)}'), '${format}')") + when (currentDialect.name) { + PostgreSQLDialect.dialectName -> { + append( + "TO_CHAR(", + exp, + " AT TIME ZONE INTERVAL '${toOffsetString(toTZ)}'", + ", '${format}')" + ) + } + + MysqlDialect.dialectName -> { + append( + "DATE_FORMAT(CONVERT_TZ(", + exp, + ", '${toOffsetString(fromTZ)}', '${toOffsetString(toTZ)}'), '${format}')" + ) + } + + else -> { + error("${currentDialect.name} is unsupported database for DateFormatWithTZFunction.") + } + } } private fun toOffsetString(tz: TimeZone): String { diff --git a/src/main/kotlin/com/tumugin/aisu/infra/repository/exposed/repository/ChekiRepositoryImpl.kt b/src/main/kotlin/com/tumugin/aisu/infra/repository/exposed/repository/ChekiRepositoryImpl.kt index da8526c1..1ef495f5 100644 --- a/src/main/kotlin/com/tumugin/aisu/infra/repository/exposed/repository/ChekiRepositoryImpl.kt +++ b/src/main/kotlin/com/tumugin/aisu/infra/repository/exposed/repository/ChekiRepositoryImpl.kt @@ -107,13 +107,13 @@ class ChekiRepositoryImpl : ChekiRepository { return transaction { val yearConvertFunc = DateFormatWithTZFunction( Chekis.shotAt, - "%Y", + "yyyy", TimeZone.of("UTC"), baseTimezone ) val monthConvertFunc = DateFormatWithTZFunction( Chekis.shotAt, - "%c", + "mm", TimeZone.of("UTC"), baseTimezone ) diff --git a/src/main/resources/db/migration/V1__create_users.sql b/src/main/resources/db/migration/V1__create_users.sql deleted file mode 100644 index aca0fc52..00000000 --- a/src/main/resources/db/migration/V1__create_users.sql +++ /dev/null @@ -1,11 +0,0 @@ -create table `users` -( - `id` bigint unsigned primary key auto_increment not null, - `name` varchar(255) not null, - `email` varchar(255) unique, - `password` varchar(255), - `email_verified_at` datetime, - `force_logout_generation` integer default 0, - `created_at` datetime not null, - `updated_at` datetime not null -); diff --git a/src/main/resources/db/migration/V1__initial_create_postgres.sql b/src/main/resources/db/migration/V1__initial_create_postgres.sql new file mode 100644 index 00000000..46a6a729 --- /dev/null +++ b/src/main/resources/db/migration/V1__initial_create_postgres.sql @@ -0,0 +1,107 @@ +create table "users" +( + "id" bigserial primary key not null, + "name" varchar(255) not null, + "email" varchar(255) unique, + "password" varchar(255), + "email_verified_at" timestamptz, + "force_logout_generation" integer default 0, + "created_at" timestamptz not null, + "updated_at" timestamptz not null +); + +create table "groups" +( + "id" bigserial primary key not null, + "user_id" bigint, + "name" varchar(255) not null, + "status" varchar(255) not null, + "created_at" timestamptz not null, + "updated_at" timestamptz not null, + constraint fk_user_id foreign key ("user_id") references "users" ("id") on delete set null +); + +create table "idols" +( + "id" bigserial primary key not null, + "user_id" bigint, + "name" varchar(255) not null, + "status" varchar(255) not null, + "created_at" timestamptz not null, + "updated_at" timestamptz not null, + constraint fk_user_id foreign key ("user_id") references "users" ("id") on delete set null +); + +create table "regulations" +( + "id" bigserial primary key not null, + "group_id" bigint not null, + "user_id" bigint, + "name" varchar(255) not null, + "comment" text not null, + "unit_price" integer not null, + "status" varchar(255) not null, + "created_at" timestamptz not null, + "updated_at" timestamptz not null, + constraint fk_group_id foreign key ("group_id") references "groups" ("id") on delete cascade, + constraint fk_user_id foreign key ("user_id") references "users" ("id") on delete set null +); + +create table "chekis" +( + "id" bigserial primary key not null, + "user_id" bigint not null, + "idol_id" bigint, + "regulation_id" bigint, + "quantity" integer not null, + "shot_at" timestamptz not null, + "created_at" timestamptz not null, + "updated_at" timestamptz not null, + constraint fk_user_id foreign key ("user_id") references "users" ("id") on delete cascade, + constraint fk_idol_id foreign key ("idol_id") references "idols" ("id") on delete set null, + constraint fk_regulation_id foreign key ("regulation_id") references "regulations" ("id") on delete set null +); + +create table "favorite_groups" +( + "id" bigserial primary key not null, + "user_id" bigint not null, + "group_id" bigint not null, + "created_at" timestamptz not null, + "updated_at" timestamptz not null, + constraint fk_user_id foreign key ("user_id") references "users" ("id") on delete cascade, + constraint fk_group_id foreign key ("group_id") references "groups" ("id") on delete cascade +); + +create table "group_idols" +( + "id" bigserial primary key not null, + "group_id" bigint not null, + "idol_id" bigint not null, + "created_at" timestamptz not null, + "updated_at" timestamptz not null, + constraint fk_group_id foreign key ("group_id") references "groups" ("id") on delete cascade, + constraint fk_idol_id foreign key ("idol_id") references "idols" ("id") on delete cascade, + unique ("group_id", "idol_id") +); + +create table "admin_users" +( + "id" bigserial primary key not null, + "name" varchar(255) not null, + "email" varchar(255) unique, + "password" varchar(255), + "force_logout_generation" integer default 0, + "created_at" timestamptz not null, + "updated_at" timestamptz not null +); + +create table "auth0_users" +( + "id" bigserial primary key not null, + "user_id" bigint not null unique, + "auth0_user_id" varchar(255) not null unique, + "created_at" timestamptz not null, + "updated_at" timestamptz not null, + constraint fk_user_id foreign key ("user_id") references "users" ("id") on delete cascade +); diff --git a/src/main/resources/db/migration/V2__create_groups.sql b/src/main/resources/db/migration/V2__create_groups.sql deleted file mode 100644 index d04b811f..00000000 --- a/src/main/resources/db/migration/V2__create_groups.sql +++ /dev/null @@ -1,10 +0,0 @@ -create table `groups` -( - `id` bigint unsigned primary key auto_increment not null, - `user_id` bigint unsigned, - `name` varchar(255) not null, - `status` varchar(255) not null, - `created_at` datetime not null, - `updated_at` datetime not null, - foreign key (`user_id`) references `users` (`id`) on delete set null -); diff --git a/src/main/resources/db/migration/V3__create_idols.sql b/src/main/resources/db/migration/V3__create_idols.sql deleted file mode 100644 index 139f7e6d..00000000 --- a/src/main/resources/db/migration/V3__create_idols.sql +++ /dev/null @@ -1,10 +0,0 @@ -create table `idols` -( - `id` bigint unsigned primary key auto_increment not null, - `user_id` bigint unsigned, - `name` varchar(255) not null, - `status` varchar(255) not null, - `created_at` datetime not null, - `updated_at` datetime not null, - foreign key (`user_id`) references `users` (`id`) on delete set null -) diff --git a/src/main/resources/db/migration/V4__create_regulations.sql b/src/main/resources/db/migration/V4__create_regulations.sql deleted file mode 100644 index 2aea96bd..00000000 --- a/src/main/resources/db/migration/V4__create_regulations.sql +++ /dev/null @@ -1,14 +0,0 @@ -create table `regulations` -( - `id` bigint unsigned primary key auto_increment not null, - `group_id` bigint unsigned not null, - `user_id` bigint unsigned, - `name` varchar(255) not null, - `comment` text not null, - `unit_price` integer not null, - `status` varchar(255) not null, - `created_at` datetime not null, - `updated_at` datetime not null, - foreign key (`group_id`) references `groups` (`id`) on delete cascade, - foreign key (`user_id`) references `users` (`id`) on delete set null -); diff --git a/src/main/resources/db/migration/V5__create_chekis.sql b/src/main/resources/db/migration/V5__create_chekis.sql deleted file mode 100644 index c25940ab..00000000 --- a/src/main/resources/db/migration/V5__create_chekis.sql +++ /dev/null @@ -1,14 +0,0 @@ -create table `chekis` -( - `id` bigint unsigned primary key auto_increment not null, - `user_id` bigint unsigned not null, - `idol_id` bigint unsigned, - `regulation_id` bigint unsigned, - `quantity` integer not null, - `shot_at` datetime not null, - `created_at` datetime not null, - `updated_at` datetime not null, - foreign key (`user_id`) references `users` (`id`) on delete cascade, - foreign key (`idol_id`) references `idols` (`id`) on delete set null, - foreign key (`regulation_id`) references `regulations` (`id`) on delete set null -) diff --git a/src/main/resources/db/migration/V6__favorite_groups.sql b/src/main/resources/db/migration/V6__favorite_groups.sql deleted file mode 100644 index c98b28d3..00000000 --- a/src/main/resources/db/migration/V6__favorite_groups.sql +++ /dev/null @@ -1,10 +0,0 @@ -create table `favorite_groups` -( - `id` bigint unsigned primary key auto_increment not null, - `user_id` bigint unsigned not null, - `group_id` bigint unsigned not null, - `created_at` datetime not null, - `updated_at` datetime not null, - foreign key (`user_id`) references `users` (`id`) on delete cascade, - foreign key (`group_id`) references `groups` (`id`) on delete cascade -) diff --git a/src/main/resources/db/migration/V7__create_group_idols.sql b/src/main/resources/db/migration/V7__create_group_idols.sql deleted file mode 100644 index d6ea5ee1..00000000 --- a/src/main/resources/db/migration/V7__create_group_idols.sql +++ /dev/null @@ -1,11 +0,0 @@ -create table `group_idols` -( - `id` bigint unsigned primary key auto_increment not null, - `group_id` bigint unsigned not null, - `idol_id` bigint unsigned not null, - `created_at` datetime not null, - `updated_at` datetime not null, - foreign key (`group_id`) references `groups` (`id`) on delete cascade, - foreign key (`idol_id`) references `idols` (`id`) on delete cascade, - unique (`group_id`, `idol_id`) -) diff --git a/src/main/resources/db/migration/V8__create_admin_users.sql b/src/main/resources/db/migration/V8__create_admin_users.sql deleted file mode 100644 index ae8e72e9..00000000 --- a/src/main/resources/db/migration/V8__create_admin_users.sql +++ /dev/null @@ -1,10 +0,0 @@ -create table `admin_users` -( - `id` bigint unsigned primary key auto_increment not null, - `name` varchar(255) not null, - `email` varchar(255) unique, - `password` varchar(255), - `force_logout_generation` integer default 0, - `created_at` datetime not null, - `updated_at` datetime not null -); diff --git a/src/main/resources/db/migration/V9__create_auth0_users.sql b/src/main/resources/db/migration/V9__create_auth0_users.sql deleted file mode 100644 index b66538dc..00000000 --- a/src/main/resources/db/migration/V9__create_auth0_users.sql +++ /dev/null @@ -1,9 +0,0 @@ -create table `auth0_users` -( - `id` bigint unsigned primary key auto_increment not null, - `user_id` bigint unsigned not null unique, - `auth0_user_id` varchar(255) not null unique, - `created_at` datetime not null, - `updated_at` datetime not null, - foreign key (`user_id`) references `users` (`id`) on delete cascade -) diff --git a/src/test/kotlin/com/tumugin/aisu/testing/BaseDatabaseTest.kt b/src/test/kotlin/com/tumugin/aisu/testing/BaseDatabaseTest.kt index 9a3f284e..fdd5781d 100644 --- a/src/test/kotlin/com/tumugin/aisu/testing/BaseDatabaseTest.kt +++ b/src/test/kotlin/com/tumugin/aisu/testing/BaseDatabaseTest.kt @@ -38,16 +38,12 @@ abstract class BaseDatabaseTest : KoinTest { private fun truncateDatabase() { transaction { - // 外部キー制約があるテーブルをTRUNCATEするために一時的に制約を取る - TransactionManager.current().exec("SET FOREIGN_KEY_CHECKS = 0") // flywayのテーブルを除いて全てのテーブルをTRUNCATEする db.dialect.allTablesNames().filterNot { it.contains("flyway_schema_history") || it.contains("FLYWAY_SCHEMA_HISTORY") }.forEach { - TransactionManager.current().exec("TRUNCATE TABLE $it") + TransactionManager.current().exec("TRUNCATE TABLE $it RESTART IDENTITY CASCADE") } - // 一時的に外していた制約を戻す - TransactionManager.current().exec("SET FOREIGN_KEY_CHECKS = 1") } } }