14

I have the code below in Play for Scala to access a SAP Hana table with Hibernate. I need to implement the same code with MySql, but the problem is that MySql doesn't support sequences (it works with AUTO_INCREMENT columns) and the code breaks because I have to specify @SequenceGenerator for Hana. Is there a way to compile this code with a condition to exclude the @SequenceGenerator annotation, so it works for MySql and Hana at the same time?

@Entity
@Table(name = "clients")
class ClientJpa {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "generator")
    @SequenceGenerator(name="generator", sequenceName = "cliSeq", allocationSize = 1)    
    var surrogateKey: Int = _
    var code: String = _
    var name: String = _
}
Steve Chambers
  • 37,270
  • 24
  • 156
  • 208
ps0604
  • 1,227
  • 23
  • 133
  • 330

5 Answers5

2

Probably not what you want to hear but AFAIK there's no way to conditionally include annotations. An alternative would be to include the common functionality in a @MappedSuperclass and inject the concrete instance as appropriate at build time depending on the environment. Something like this:-

@MappedSuperclass
abstract class AbstractClientJpa {
    var surrogateKey: Int   // abstract
    var code: String = _
    var name: String = _
}

...

@Entity
@Table(name = "clients")
class HanaClientJpa extends AbstractClientJpa {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "generator")
    @SequenceGenerator(name="generator", sequenceName = "cliSeq", allocationSize = 1)    
    var surrogateKey: Int = _
}

...

@Entity
@Table(name = "clients")
class MySQLClientJpa extends AbstractClientJpa {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var surrogateKey: Int = _
}
Steve Chambers
  • 37,270
  • 24
  • 156
  • 208
2

Assuming I understand your problem correctly I have 2 potential solutions for you. Both are general ideas and you'll have to do some legwork to actually implement them.

  1. Use macros. Here is a bit old article that does some AST manipulation to enrich case classes. You should be able to do something in that vein for your case. Here is a way pass parameters to your macro at compile time. Main con with this route is that macro api was scala version dependent, somewhat messy, unstable and hard to find good documentation for last time I checked.

  2. Use AspectJ. You should be able to declare annotations you need on classes in build-time. Main con here is that you'll have to add AspectJ weaving to your build which may or may not be easy.

Eugene Loy
  • 12,224
  • 8
  • 53
  • 79
  • 1
    I have attempted to implement your suggestion and provided it as an answer. I hope that is ok. If not feel free to paste the code in your answer and I could remove mine. https://stackoverflow.com/questions/47995473/how-to-conditionally-include-a-hibernate-annotation#answer-50666569 – Mario Galic Jun 03 '18 at 12:49
  • @MarioGalic I am ok with this. – Eugene Loy Jun 03 '18 at 16:01
2

This answer attempts to implement Eugene's suggestion (so if it works please give credit to Eugene).

Given the following definition of @ifNotMysql macro

import scala.reflect.macros.blackbox
import scala.language.experimental.macros
import scala.annotation.{StaticAnnotation, compileTimeOnly}

object ifNotMysqlMacro {
  val targetIsMySql = sys.props.get("target-mysql").contains("true")

  def impl(c: blackbox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
    import c.universe._

    def mysqlAnnots(annotations: Seq[c.universe.Tree]): Seq[c.universe.Tree] =
      annotations
        .filterNot(_.toString.contains("SequenceGenerator"))
        .filterNot(_.toString.contains("GeneratedValue"))
        .:+(q"""new GeneratedValue(strategy = GenerationType.IDENTITY)""")

    val result = annottees.map(_.tree).toList match {
      case q"@..$annots var $pat: $tpt = $expr" :: Nil =>
        q"""
            @..${if (targetIsMySql) mysqlAnnots(annots) else annots}
            var $pat: $tpt = $expr
          """
    }
    c.Expr[Any](result)
  }
}

@compileTimeOnly("enable macro paradise to expand macro annotations")
class ifNotMysql extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro ifNotMysqlMacro.impl
}

if we write @ifNotMysql @GeneratedValue(...) @SequenceGenerator like so

@ifNotMysql 
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "generator")
@SequenceGenerator(name="generator", sequenceName = "cliSeq", allocationSize = 1)    
var surrogateKey: Int = _

and provide system property target-mysql like so

sbt -Dtarget-mysql=true compile

then @SequenceGenerator annotation will be excluded and @GeneratedValue(strategy = GenerationType.IDENTITY) added like so

@GeneratedValue(strategy = GenerationType.IDENTITY)
var surrogateKey: Int = _

This implementation is based on scalamacros/sbt-example-paradise

Mario Galic
  • 47,285
  • 6
  • 56
  • 98
  • `@GeneratedValue` also needs to change depending on whether the compilation is on Hana or MySql, it seems that your solution handles only `@SequenceGenerator` – ps0604 Jun 03 '18 at 18:59
  • If environment is MySql then the following line should be inserted instead of the one in the example: `@GeneratedValue(strategy = GenerationType.IDENTITY)` – ps0604 Jun 03 '18 at 19:15
  • Mario, inserting a row in MySQL throws an error, are you sure it's replacing with `GenerationType.IDENTITY` when MySQL is set? – ps0604 Jun 04 '18 at 16:59
  • @ps0604 What is the error? Perhaps it is because I did not provide `generator` argument to `@GeneratedValue`, that is, `@GeneratedValue(strategy = GenerationType.IDENTITY, generator = "generator")`. I have edited the answer, please try again. – Mario Galic Jun 05 '18 at 16:57
  • I still get the same error (by the way, I don't declare `generator` argument when using MySQL). The error seems to be related to a sequence that cannot read to generate the ID: `Exception: org.hibernate.exception.SQLGrammarException: could not extract ResultSet) (of class scala.util.Failure)]]` – ps0604 Jun 05 '18 at 19:48
  • 1
    By the way, if I compile in MySQL removing Hana annotations, this works fine with `@GeneratedValue(strategy = GenerationType.IDENTITY)` and without the `generator` argument. – ps0604 Jun 05 '18 at 19:50
  • I checked yours as the correct answer as the bounty period ended. – ps0604 Jun 05 '18 at 20:01
  • [Printing](https://www.scala-lang.org/api/2.12.x/scala-reflect/scala/reflect/api/Printers.html) the AST via `show(result)` I can confirm `@GeneratedValue(strategy = GenerationType.IDENTITY)` is present. – Mario Galic Jun 05 '18 at 20:54
  • @ps0604 Have you double checked `-Dtarget-mysql=true` is being picked up? Try printing the property value from some other class. – Mario Galic Jun 05 '18 at 20:58
  • for my test I simply set `val targetIsMySql = true` to make sure – ps0604 Jun 05 '18 at 23:23
  • I'm using scala 2.11, not 2.12, could that be the problem? – ps0604 Jun 16 '18 at 21:26
  • Add `@compileTimeOnly("enable macro paradise to expand macro annotations")` above `class ifNotMysql ...` as shown in the edited answer, and recompile. Do you get a compiler error? – Mario Galic Jun 19 '18 at 22:19
  • I get `not found: type compileTimeOnly` isn't this a 2.12 class? By the way, I'm using Play 2.5 and cannot upgrade to Scala 2.12 – ps0604 Jun 19 '18 at 23:01
  • You might be missing an `import scala.annotation.compileTimeOnly` – Mario Galic Jun 19 '18 at 23:19
  • The `ifNotMysql` macro should work in both 2.11 and 2.12. – Mario Galic Jun 19 '18 at 23:20
  • I added the import and the @compileTimeOnly annotation, and get a compilation error in `var surrogateKey: Int = _` – ps0604 Jun 19 '18 at 23:25
  • Ok good, you are probably missing `addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)` in your `build.sbt`. According to [docs](https://docs.scala-lang.org/overviews/macros/annotations.html): *"Macro annotations look like normal annotations to the vanilla Scala compiler, so if you forget to enable the macro paradise plugin in your build, your annotations will silently fail to expand."* – Mario Galic Jun 19 '18 at 23:32
  • Added the `addCompilerPlugin` and now I get `macro annotation could not be expanded (the most common reason for that is that you need to enable the macro paradise plugin; another possibility is that you try to use macro annotation in the same compilation run that defines it)` – ps0604 Jun 20 '18 at 00:39
  • Maybe Play is causing the problem? – ps0604 Jun 20 '18 at 00:51
  • Macro needs to be defined in its own module: https://stackoverflow.com/questions/32021707/what-does-it-mean-when-macro-annotation-cannot-be-used-in-the-same-compilation-t – Mario Galic Jun 20 '18 at 19:57
  • If by module you mean a separate source code file containing the macro, then that's how I did it, it's even in a different package – ps0604 Jun 21 '18 at 00:25
1

I think the way to do it is to provide a custom IdGeneratorStrategyInterpreter and register it using MetadataBuilder.applyIdGenerationTypeInterpreter. In your custom IdGeneratorStrategyInterpreter you can override determineGeneratorName to return "identity" constant for GenerationType.SEQUENCE if you know that the code is run against MySql and return null in all other cases to let the FallbackInterpreter do its default job (the string "identity" also comes from FallbackInterpreter.determineGeneratorName implementation). And you can do nothing in other methods and let the FallbackInterpreter do it's usual job.

P.S. Please also note that Hibernate's default SequenceStyleGenerator is actually aware of DBs not supporting "sequences" (exposed via Dialect.supportsSequences) and is able to emulate similar behavior using additional table. This might or might not be OK for your scenario.

SergGr
  • 23,570
  • 2
  • 30
  • 51
0

If ID in mysql is given auto-increment then in hibernate mapping ID should be IDENTITY

Replace that with

@Entity
@Table(name="clients")
class ClientJpa{
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "generator")
  var surrogateKey: Int = _
  var code: String = _
  var name: String = _

 }

Hope it works....

Jyoti Jadhav
  • 307
  • 2
  • 18