Reputation: 2521
I'm extremely new to String Boot and backend development (maybe three days or less) and I have the desire to build REST API to consume from different clients.
So I started by a simple demo app that has an endpoint called /register
. We post a JSON
string with username
and password
to create a new user if not exist.
I was using JPA
with HSQLDB
and it worked fine persisting on memory. But recently I wanted to use RxJava
since I'm familiar with on Android, so I switched to R2DBC
with MySQL
.
MySQL
server is running fine on port 3306
and the app was tested using PostMan on localhost:8080
The problem occurs when I try to query users table or insert entities and it looks like this:
{
"timestamp": "2020-03-22T11:54:43.466+0000",
"status": 500,
"error": "Internal Server Error",
"message": "execute; bad SQL grammar [UPDATE user_entity SET username = $1, password = $2 WHERE user_entity.id = $3]; nested exception is io.r2dbc.spi.R2dbcBadGrammarException: [42102] [42S02] Table \"USER_ENTITY\" not found; SQL statement:\nUPDATE user_entity SET username = $1, password = $2 WHERE user_entity.id = $3 [42102-200]",
"path": "/register"
}
Here's the full logfile for the exception.
I Have been looking for a solution for hours and I seem like not finding it anywhere, so I hope that I will find it here.
1. database:
2. application.properties:
logging.level.org.springframework.data.r2dbc=DEBUG
spring.datasource.url=jdbc:mysql://localhost:3306/demodb
spring.datasource.username=root
spring.datasource.password=root
3. DatabaseConfiguration:
@Configuration
@EnableR2dbcRepositories
class DatabaseConfiguration : AbstractR2dbcConfiguration() {
override fun connectionFactory(): ConnectionFactory
= ConnectionFactories.get(
builder().option(DRIVER, "mysql")
.option(HOST, "localhost")
.option(USER, "root")
.option(PASSWORD, "root")
.option(DATABASE, "demodb")
.build()
)
}
4. RegistrationController:
@RequestMapping("/register")
@RestController
class RegistrationController @Autowired constructor(private val userService: UserService) {
@PostMapping
fun login(@RequestBody registrationRequest: RegistrationRequest): Single<ResponseEntity<String>>
= userService.userExists(registrationRequest.username)
.flatMap { exists -> handleUserExistance(exists, registrationRequest) }
private fun handleUserExistance(exists: Boolean, registrationRequest: RegistrationRequest): Single<ResponseEntity<String>>
= if (exists) Single.just(ResponseEntity("Username already exists. Please try an other one", HttpStatus.CONFLICT))
else userService.insert(User(registrationRequest.username, registrationRequest.password)).map { user ->
ResponseEntity("User was successfully created with the id: ${user.id}", HttpStatus.CREATED)
}
}
5. UserService:
@Service
class UserService @Autowired constructor(override val repository: IRxUserRepository) : RxSimpleService<User, UserEntity>(repository) {
override val converter: EntityConverter<User, UserEntity> = UserEntity.Converter
fun userExists(username: String): Single<Boolean>
= repository.existsByUsername(username)
}
6. RxSimpleService:
abstract class RxSimpleService<T, E>(protected open val repository: RxJava2CrudRepository<E, Long>) {
protected abstract val converter: EntityConverter<T, E>
open fun insert(model: T): Single<T>
= repository.save(converter.fromModel(model))
.map(converter::toModel)
open fun get(id: Long): Maybe<T>
= repository.findById(id)
.map(converter::toModel)
open fun getAll(): Single<ArrayList<T>>
= repository.findAll()
.toList()
.map(converter::toModels)
open fun delete(model: T): Completable
= repository.delete(converter.fromModel(model))
}
7. RxUserRepository:
@Repository
interface IRxUserRepository : RxJava2CrudRepository<UserEntity, Long> {
@Query("SELECT CASE WHEN EXISTS ( SELECT * FROM ${UserEntity.TABLE_NAME} WHERE username = :username) THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT) END")
fun existsByUsername(username: String): Single<Boolean>
}
8. And finally, here's my UserEntity
@Table(TABLE_NAME)
data class UserEntity(
@Id
val id: Long,
val username: String,
val password: String
) {
companion object {
const val TABLE_NAME = "user_entity"
}
object Converter : EntityConverter<User, UserEntity> {
override fun fromModel(model: User): UserEntity
= with(model) { UserEntity(id, username, password) }
override fun toModel(entity: UserEntity): User
= with(entity) { User(id, username, password) }
}
}
User
and RegistrationRequest
are just simple objects with username and password.
What I have missed?
Please leave a comment if you need more code.
Upvotes: 6
Views: 11342
Reputation: 2521
I finally managed to solve this mistake!
The problems were so simple yet so sneaky for a beginner:
JDBC
in my URL instead of R2DBC
H2
runtime implementation so it was
expecting an H2
in-memory databaseConnectionFactory
was not very correctSo what I did was the following:
build.gradle
:
implementation("io.r2dbc:r2dbc-pool")
, implementation("dev.miku:r2dbc-mysql:0.8.1.RELEASE")
and
runtimeOnly("mysql:mysql-connector-java")
runtimeOnly("io.r2dbc:r2dbc-h2")
It now looks like this:
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.2.5.RELEASE"
id("io.spring.dependency-management") version "1.0.9.RELEASE"
kotlin("jvm") version "1.3.61"
kotlin("plugin.spring") version "1.3.61"
}
group = "com.tamimattafi.backend"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
repositories {
mavenCentral()
maven(url = "https://repo.spring.io/milestone")
}
dependencies {
//SPRING BOOT
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot.experimental:spring-boot-starter-data-r2dbc")
//KOTLIN
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
//RX JAVA
implementation("io.reactivex.rxjava2:rxjava:2.2.0")
implementation("io.reactivex:rxjava-reactive-streams:1.2.1")
//MYSQL
implementation("dev.miku:r2dbc-mysql:0.8.1.RELEASE")
implementation("io.r2dbc:r2dbc-pool")
runtimeOnly("mysql:mysql-connector-java")
//TEST
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
testImplementation("org.springframework.security:spring-security-test")
testImplementation("io.projectreactor:reactor-test")
testImplementation("org.springframework.boot.experimental:spring-boot-test-autoconfigure-r2dbc")
}
dependencyManagement {
imports {
mavenBom("org.springframework.boot.experimental:spring-boot-bom-r2dbc:0.1.0.M3")
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "1.8"
}
}
Updated my application.properties
to this:
spring.r2dbc.url=r2dbc:pool:mysql://127.0.0.1:3306/demodb
spring.r2dbc.username=root
spring.r2dbc.password=root
Updated my DatabaseConfiguration
to this (Note that I removed the @EnableR2dbcRepositories
because it should be elsewhere) :
@Configuration
class DatabaseConfiguration : AbstractR2dbcConfiguration() {
override fun connectionFactory(): ConnectionFactory
= MySqlConnectionFactory.from(
MySqlConnectionConfiguration.builder()
.host("127.0.0.1")
.username("root")
.port(3306)
.password("root")
.database("demodb")
.connectTimeout(Duration.ofSeconds(3))
.useServerPrepareStatement()
.build()
)
}
Updated my Application
class (I brought the annotation here):
@SpringBootApplication
@EnableR2dbcRepositories
class DemoApplication
fun main(args: Array<String>) {
runApplication<DemoApplication>(*args)
}
Upvotes: 7
Reputation: 729
In application.properties
you need to set the spring.jpa.hibernate.ddl-auto
property.
The options are:
validate: validate the schema, makes no changes to the database.
update: update the schema.
create: creates the schema, destroying previous data.
create-drop: drop the schema when the SessionFactory is closed explicitly, typically when the application is stopped.
none: does nothing with the schema, makes no changes to the database
Upvotes: 0