How to persist enums as ordinals with Spring Boot and Cassandra?

有些话、适合烂在心里 提交于 2020-01-05 04:15:08

问题


To the enum field of my entity I have added @CassandraType(type = DataType.Name.INT). However not the ordinal of the enum, but the string representation instead, is used in the statement sent to Cassandra. Thus I get the following error:

org.springframework.data.cassandra.CassandraInvalidQueryException: SessionCallback; CQL [INSERT INTO thing (thing_id,some_enum) VALUES (1,'Foo');]; Expected 4 or 0 byte int (3); nested exception is com.datastax.driver.core.exceptions.InvalidQueryException: Expected 4 or 0 byte int (3)

Below you can find a minimal example, reproducing the problem.

What am I doing wrong?

test/src/main/kotlin/enumtest/Application.kt

package enumtest

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication


@SpringBootApplication
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}

test/src/main/kotlin/enumtest/SomeEnum.kt

package enumtest


enum class SomeEnum {
    Foo,
    Bar
}

test/src/main/kotlin/enumtest/Thing.kt

package enumtest

import com.datastax.driver.core.DataType
import org.springframework.data.cassandra.core.cql.PrimaryKeyType
import org.springframework.data.cassandra.core.mapping.CassandraType
import org.springframework.data.cassandra.core.mapping.Column
import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn
import org.springframework.data.cassandra.core.mapping.Table


@Table("thing")
@Suppress("unused")
class Thing(

    @PrimaryKeyColumn(name = "thing_id", ordinal = 0, type = PrimaryKeyType.PARTITIONED)
    var thingId: Long,

    @CassandraType(type = DataType.Name.INT)
    @Column("some_enum")
    var someEnum: SomeEnum

)

test/src/main/kotlin/enumtest/ThingRepository.kt

package enumtest

import org.springframework.data.cassandra.repository.CassandraRepository
import org.springframework.stereotype.Repository

@Repository
interface ThingRepository : CassandraRepository<Thing, Long>

test/src/main/resources/application.yml

spring:
  data:
    cassandra:
      contact-points: localhost
      port: 9142
      keyspace_name: enumtest

test/src/test/kotlin/enumtest/PersistenceTest.kt

package enumtest

import org.cassandraunit.spring.CassandraDataSet
import org.cassandraunit.spring.CassandraUnitDependencyInjectionTestExecutionListener
import org.cassandraunit.spring.EmbeddedCassandra
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.TestExecutionListeners
import org.springframework.test.context.junit4.SpringRunner

@RunWith(SpringRunner::class)
@SpringBootTest
@TestExecutionListeners(
    listeners = [CassandraUnitDependencyInjectionTestExecutionListener::class],
    mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
)
@CassandraDataSet(value = ["cql/cassandra_schema.cql"], keyspace = "enumtest")
@EmbeddedCassandra
class PersistenceTest {
    @Autowired
    lateinit var thingRepository: ThingRepository

    @Test
    fun `test save`() {
        thingRepository.save(Thing(1, SomeEnum.Foo))
        val things = thingRepository.findAll()
        Assert.assertEquals(1, things.size)
        val thing = things[0]
        Assert.assertEquals(SomeEnum.Foo, thing.someEnum)
    }
}

test/src/test/resources/cql/cassandra_schema.cql

CREATE KEYSPACE IF NOT exists enumtest
WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':1};

CREATE TABLE IF NOT exists enumtest.thing (
    thing_id     bigint,
    some_enum    int,
    PRIMARY KEY (thing_id)
);

test/build.gradle

plugins {
    id 'org.springframework.boot' version '2.1.4.RELEASE'
    id 'org.jetbrains.kotlin.jvm' version '1.3.30'
    id 'org.jetbrains.kotlin.plugin.spring' version '1.3.30'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
    mavenCentral()
    maven { url "https://repository.apache.org/snapshots/" }
}

dependencies {
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter'
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-cassandra'
    implementation group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8'
    implementation group: 'org.jetbrains.kotlin', name: 'kotlin-reflect'

    testImplementation group: 'org.cassandraunit', name: 'cassandra-unit-spring', version: '3.5.0.1'
    testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test'
}

compileKotlin {
    kotlinOptions {
        freeCompilerArgs = ['-Xjsr305=strict']
        jvmTarget = '1.8'
    }
}

compileTestKotlin {
    kotlinOptions {
        freeCompilerArgs = ['-Xjsr305=strict']
        jvmTarget = '1.8'
    }
}

Here is the full version of the minimal example as a download to faciliate experimentation: https://drive.google.com/open?id=1zzIDhbWycaj4WXrze2sAmw8xRPacA8Js

Edit: Since it seems to be a bug, I just opened a Jira issue.


回答1:


I've been trying to get this working for quite awhile and it seems I finally got it!

I was running into the same issue you were with the codec...I have no idea why that's not working. According to their documentation you were doing it exactly right.

So I implemented my own Cassandra Write Converter. See below

@Configuration
class CassandraConfig(val cluster: Cluster){

    @Bean
    fun setCustomCassandraConversions() = CassandraCustomConversions(listOf(EnumWriteConverter.INSTANCE, EnumReadConverter.INSTANCE))

    @WritingConverter
    enum class EnumWriteConverter : Converter<Enum<MyEnum>, Int> {
        INSTANCE;
        override fun convert(source: Enum<MyEnum>) = source.ordinal
    }
     @ReadingConverter
    enum class EnumReadConverter : Converter<Int, Enum<MyEnum>> {
        INSTANCE;
        override fun convert(source: Int) = MyEnum.values()[source]
    }
}

This should on every write you do to Cassandra convert all enums it sees of type MyEnum to an Int using the overridden converter. This opens you up to the possibility of having multiple of these for different types of Enums where maybe for some reason you would like to write other custom values from them instead of always converting all enums.

Hope this works!

EDIT Note the change in removing { } for INSTANCE on each converter, and registering the ReadingConverter with the CassandraCustomConversions




回答2:


This is fixed since Spring Boot version 2.1.5.

However, the @CassandraType needs to be placed at the getter explicitly in Kotlin, because otherwise it is not seen at runtime.

In practice this simply means replacing this:

    @CassandraType(type = DataType.Name.INT)
    var someEnum: SomeEnum

with that:

    @get: CassandraType(type = DataType.Name.INT)
    var someEnum: SomeEnum


来源:https://stackoverflow.com/questions/55915849/how-to-persist-enums-as-ordinals-with-spring-boot-and-cassandra

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!