Использование автоматически увеличивающихся полей с PostgreSQL и Slick

Как вставить записи в PostgreSQL, используя ключи AutoInc с сопоставленными таблицами Slick? Если я использую и Option для идентификатора в моем классе case и устанавливаю его в None, тогда PostgreSQL будет жаловаться при вставке, что поле не может быть пустым. Это работает для H2, но не для PostgreSQL:

//import scala.slick.driver.H2Driver.simple._
//import scala.slick.driver.BasicProfile.SimpleQL.Table
import scala.slick.driver.PostgresDriver.simple._
import Database.threadLocalSession

object TestMappedTable extends App{

    case class User(id: Option[Int], first: String, last: String)

    object Users extends Table[User]("users") {
        def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
        def first = column[String]("first")
        def last = column[String]("last")
        def * = id.? ~ first ~ last <> (User, User.unapply _)
        def ins1 = first ~ last returning id
        val findByID = createFinderBy(_.id)
        def autoInc = id.? ~ first ~ last <> (User, User.unapply _) returning id
    }

 // implicit val session = Database.forURL("jdbc:h2:mem:test1", driver = "org.h2.Driver").createSession()
    implicit val session = Database.forURL("jdbc:postgresql:test:slicktest",
                           driver="org.postgresql.Driver",
                           user="postgres",
                           password="xxx")

  session.withTransaction{
    Users.ddl.create

    // insert data
    print(Users.insert(User(None, "Jack", "Green" )))
    print(Users.insert(User(None, "Joe", "Blue" )))
    print(Users.insert(User(None, "John", "Purple" )))
    val u = Users.insert(User(None, "Jim", "Yellow" ))
  //  println(u.id.get)
    print(Users.autoInc.insert(User(None, "Johnathan", "Seagul" )))
  }
  session.withTransaction{
    val queryUsers = for {
    user <- Users
  } yield (user.id, user.first)
  println(queryUsers.list)

  Users.where(_.id between(1, 2)).foreach(println)
  println("ID 3 -> " + Users.findByID.first(3))
  }
}

Использование вышеприведенного с H2 успешно, но если я закомментирую его и перейду на PostgreSQL, я получу:

[error] (run-main) org.postgresql.util.PSQLException: ERROR: null value in column "id" violates not-null constraint
org.postgresql.util.PSQLException: ERROR: null value in column "id" violates not-null constraint

person Jack    schedule 02.11.2012    source источник
comment
этот пример может быть кому-то полезен: github.com/slick/slick-examples/blob/2.0.0-RC1/src/main/scala/   -  person Răzvan Flavius Panda    schedule 25.12.2013


Ответы (7)


Здесь работает:

object Application extends Table[(Long, String)]("application") {   
    def idlApplication = column[Long]("idlapplication", O.PrimaryKey, O.AutoInc)
    def appName = column[String]("appname")
    def * = idlApplication ~ appName
    def autoInc = appName returning idlApplication
}

var id = Application.autoInc.insert("App1")

Вот как выглядит мой SQL:

CREATE TABLE application
(idlapplication BIGSERIAL PRIMARY KEY,
appName VARCHAR(500));

Обновление:

Конкретная проблема с сопоставленной таблицей с пользователем (как в вопросе) может быть решена следующим образом:

  def forInsert = first ~ last <>
    ({ (f, l) => User(None, f, l) }, { u:User => Some((u.first, u.last)) })

Это из тестовые примеры в репозитории Slick git.

person Bruno    schedule 04.11.2012
comment
Извините, я новичок в Scala, я думаю, что неправильно понял ваш пример. Я использую версию драйвера 9.1-901-1.jdbc4. Я отредактировал свой ответ, чтобы проиллюстрировать, что именно я здесь делаю. Надеюсь, поможет. - person Bruno; 11.11.2012
comment
@JacobusR, вы решили проблему с нулевым идентификатором при отправке, как в вашем вопросе, с None? Потому что у меня тоже есть такая проблема... - person Cristian Boariu; 15.11.2012
comment
@JacobusR Большое спасибо! Мой вопрос также заключается в том, если, например, ваше last будет значением int как Option[Int], как бы вы его определили? Другой forInsert для этого конкретного случая? Потому что, если я определяю его как Option[Int], я должен ставить None каждый раз, когда добавляю вставку, хотя бывают случаи, когда это не None... - person Cristian Boariu; 16.11.2012
comment
Привет, Кристиан, я не уверен, что понимаю... Не могли бы вы задать его как отдельный вопрос, чтобы вы могли опубликовать большую часть своего кода. Если повезет, какой-то другой умный гуру Slick наткнется на ваш вопрос и направит нас обоих прямо :-) - person Jack; 16.11.2012
comment
Просто заметка. Если ваш тип данных — int или bigint, и вы хотите использовать AutoInc, вам нужно создать последовательность и установить значение по умолчанию для использования этой последовательности. - person Travis Stevens; 03.06.2013
comment
Что можно сделать, когда таблицы больше? может 15-20 колонок... метод forInsert становится монстром! - person AlexBrand; 13.07.2013
comment
Должен согласиться с @alexBrand, это решение довольно глупо, когда у вас есть таблицы с большим количеством столбцов, количество шаблонов смехотворно. Slick должен автоматически обрабатывать случаи Option[Long] и id.? при работе с postgres. - person mdedetrich; 24.09.2013
comment
Кто-нибудь пытался заставить это работать с Slick 2.0.1? Синтаксис проекции по умолчанию изменился, поэтому приведенный выше код не компилируется, и я не могу найти ни одного рабочего примера с Postgres. - person Daghan ---; 12.04.2014

Я решил эту проблему по-другому. Поскольку я ожидаю, что мои объекты User всегда будут иметь идентификатор в логике моего приложения, и единственный момент, когда его не будет, - это во время вставки в базу данных, я использую вспомогательный класс case NewUser, у которого нет идентификатора.

case class User(id: Int, first: String, last: String)
case class NewUser(first: String, last: String)

object Users extends Table[User]("users") {
  def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
  def first = column[String]("first")
  def last = column[String]("last")

  def * = id ~ first ~ last <> (User, User.unapply _)
  def autoInc = first ~ last <> (NewUser, NewUser.unapply _) returning id
}

val id = Users.autoInc.insert(NewUser("John", "Doe"))

Опять же, User сопоставляется 1:1 с записью/строкой базы данных, тогда как NewUser можно заменить кортежем, если вы хотите избежать дополнительного класса case, поскольку он используется только как контейнер данных для вызова insert.

EDIT: Если вам нужна большая безопасность (с несколько большей детализацией), вы можете использовать трейт для классов case, например:

trait UserT {
  def first: String
  def last: String
}
case class User(id: Int, first: String, last: String) extends UserT
case class NewUser(first: String, last: String) extends UserT
// ... the rest remains intact

В этом случае вы сначала примените изменения модели к трейту (включая любые примеси, которые вам могут понадобиться) и, возможно, добавите значения по умолчанию в файл NewUser.

Мнение автора: я по-прежнему предпочитаю решение без признаков, поскольку оно более компактно, а изменения в модели заключаются в копировании и вставке параметров User с последующим удалением id (первичный ключ auto-inc), как в случае объявление класса и в табличных проекциях.

person Leonidas A    schedule 17.03.2013
comment
+1 Спасибо, что поделились. Дублирование членов не является идеальным (можно учитывать трейты или наследование), но оно делает код очень читабельным. - person Jack; 17.03.2013

Мы используем немного другой подход. Вместо того, чтобы создавать дополнительную проекцию, мы запрашиваем следующий идентификатор для таблицы, копируем его в класс case и используем проекцию по умолчанию «*» для вставки записи в таблицу.

Для постгреса это выглядит так:

Пусть ваши Table-Objects реализуют эту черту

trait TableWithId { this: Table[_] =>
  /**
   * can be overriden if the plural of tablename is irregular
   **/
  val idColName: String = s"${tableName.dropRight(1)}_id"
  def id = column[Int](s"${idColName}", O.PrimaryKey, O.AutoInc)
  def getNextId = (Q[Int] + s"""select nextval('"${tableName}_${idColName}_seq"')""").first
  }

Всем вашим классам сущностей нужен такой метод (также должен быть определен в трейте):

case class Entity (...) {
  def withId(newId: Id): Entity = this.copy(id = Some(newId)
}

Теперь новые объекты можно вставлять следующим образом:

object Entities extends Table[Entity]("entities") with TableWithId {
  override val idColName: String = "entity_id"
  ...
  def save(entity: Entity) = this insert entity.withId(getNextId) 
}

Код все еще не СУХОЙ, потому что вам нужно определить метод withId для каждой таблицы. Кроме того, вы должны запросить следующий идентификатор, прежде чем вставлять объект, который может привести к снижению производительности, но не должен быть заметным, если вы не вставите тысячи записей за раз.

Основное преимущество заключается в том, что нет необходимости во второй проекции, что делает код менее подверженным ошибкам, особенно для таблиц с большим количеством столбцов.

person alexx    schedule 05.09.2013
comment
Интересный подход, но привязывает приложение напрямую к Postgres. Подход, независимый от базы данных, заключается в использовании класса Sequence Slick и реализации каждого драйвера в соответствии с соглашением об именовании последовательностей (например, Postgres' — это tableName_pkName_seq). К сожалению, пакетные вставки не будут так хорошо работать с nextval. Не уверен, что текущий Slick (3.0) решает стандартную проблему, указанную в комментариях к принятым ответам; если нет, довольно отвратительно вручную отображать класс case как кортеж без столбца pk... - person virtualeyes; 06.04.2015

Самым простым решением было использовать тип SERIAL следующим образом:

def id = column[Long]("id", SqlType("SERIAL"), O.PrimaryKey, O.AutoInc)

Вот более конкретный блок:

// A case class to be used as table map
case class CaseTable( id: Long = 0L, dataType: String, strBlob: String)

// Class for our Table
class MyTable(tag: Tag) extends Table[CaseTable](tag, "mytable") {
  // Define the columns
  def dataType = column[String]("datatype")
  def strBlob = column[String]("strblob")

  // Auto Increment the id primary key column
  def id = column[Long]("id", SqlType("SERIAL"),  O.PrimaryKey,  O.AutoInc)

  // the * projection (e.g. select * ...) auto-transforms the tupled column values
  def * = (id, dataType, strBlob) <> (CaseTable.tupled, CaseTable.unapply _)

}


// Insert and  get auto incremented primary key
def insertData(dataType: String, strBlob: String, id: Long = 0L): Long = {
  // DB Connection
  val db = Database.forURL(jdbcUrl, pgUser, pgPassword, driver = driverClass)
  // Variable to run queries on our table
  val myTable = TableQuery[MyTable]

  val insert = try {
    // Form the query
    val query = myTable returning myTable.map(_.id) += CaseTable(id, dataType, strBlob)
    // Execute it and wait for result
    val autoId = Await.result(db.run(query), maxWaitMins)
    // Return ID
    autoId
  }
  catch {
    case e: Exception => {
      logger.error("Error in inserting using Slick: ", e.getMessage)
      e.printStackTrace()
      -1L
    }
  }
  insert
}
person Abhishek Jangalwa    schedule 15.12.2019

Я столкнулся с той же проблемой, пытаясь сделать образец компьютерной базы данных из play-slick-3.0, когда я изменил базу данных на Postgres. Что решило проблему, так это изменить тип столбца id (первичный ключ) на SERIAL в файле эволюции /conf/evolutions/default/1.sql (изначально был в BIGINT). Взгляните на https://groups.google.com/forum/?fromgroups=#%21topic/scalaquery/OEOF8HNzn2U
для всего обсуждения. Привет, Ренекс

person Rene Cejas Bolecek    schedule 27.09.2015

Еще одна хитрость заключается в том, чтобы сделать идентификатор класса case переменным.

case class Entity(var id: Long)

Чтобы вставить экземпляр, создайте его, как показано ниже Entity(null.asInstanceOf[Long])

Я проверил, что это работает.

person James    schedule 01.08.2016

Решение, которое я нашел, состоит в том, чтобы использовать SqlType("Serial") в определении столбца. Я еще не тестировал его широко, но, похоже, он работает до сих пор.

Итак, вместо

def id: Rep[PK[SomeTable]] = column[PK[SomeTable]]("id", O.PrimaryKey, O.AutoInc)

Ты должен сделать:

def id: Rep[PK[SomeTable]] = column[PK[SomeTable]]("id", SqlType("SERIAL"), O.PrimaryKey, O.AutoInc)

Где PK определяется как пример в книге "Essential Slick":

final case class PK[A](value: Long = 0L) extends AnyVal with MappedTo[Long]
person Alex Klibisz    schedule 06.07.2019