Подтвердить что ты не робот

Как вставить значение enum Postgres с помощью Clojure JDBC?

Например, вот таблица продуктов в PostgreSQL со статусом enum:

create type product_status as enum ('InStock', 'OutOfStock');

create table product (
    pid            int primary key default nextval('product_pid_seq'),
    sku            text not null unique,
    name           text not null,
    description    text not null,
    quantity       int not null,
    cost           numeric(10,2) not null,
    price          numeric(10,2) not null,
    weight         numeric(10,2),
    status         product_status not null
);

Типичным Clojure кодом для вставки продукта будет:

(def prod-12345 {:sku "12345"
                 :name "My Product"
                 :description "yada yada yada"
                 :quantity 100
                 :cost 42.00
                 :price 59.00
                 :weight 0.3
                 :status "InStock"})

(sql/with-connection db-spec
   (sql/insert-record :product prod-12345))

Однако status - это перечисление, поэтому вы не можете вставлять его как обычную строку, не перебрасывая ее в перечисление:

'InStock'::product_status

Я знаю, что вы можете сделать это с помощью подготовленного заявления, например:

INSERT INTO product (name, status) VALUES (?, ?::product_status)

Но есть ли способ сделать это без использования подготовленного заявления?

4b9b3361

Ответ 1

Я получил эту работу сегодня, используя обходное решение stringtype=unspecified hack.

Вы можете добавить этот параметр в свой db-spec следующим образом:

(def db-spec {:classname "org.postgresql.Driver"
              :subprotocol "postgresql"
              :subname "//myserver:5432/mydatabase"
              :user "myuser"
              :password "mypassword"
              :stringtype "unspecified"}) ; HACK to support enums

Затем просто используйте insert!, как обычно.

Было бы хорошо иметь решение, которое не так сильно ослабляет безопасность типов.

Ответ 2

Крис Юрка ответил на обсуждение Майка Шеррилла, процитированное выше, с обходным путем:

используйте параметр url stringtype = unspecified [в URL-адресе JDBC-соединения], чтобы setString всегда связывалась с неизвестным, а не с varchar, что тогда не требовало каких-либо изменений кода.

Я пробовал это на Java, и, похоже, он работает нормально.

Ответ 3

Если вы не передадите простой SQL на задний план, вам придется использовать листинг. (Оператор SQL INSERT INTO product (name, status) VALUES ('SomeName', 'InStock'); должен работать нормально.)

Том Лейн рассмотрел эту проблему на pgsql-хакерах через неделю после того, как вы задали свой вопрос.

AFAIK - это обычное дело с JDBC: setString() подразумевает, что параметр имеет строковый тип. Это упадет, если тип на самом деле требуется не что иное, как строка. (Я не эксперт по Java, но я, похоже, напомним, что использование setObject вместо этого является стандартным обходным решением.)

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

Наш собственный @CraigRinger участвовал в этом обсуждении и, возможно, уже нашел что-то актуальное.

Ответ 4

Этот пост блога решает проблему красиво. jdbc предоставляет протокол ISQLValue, который имеет только один метод, sql-value который преобразует значение clojure в значение sql, представленное PGObject. В сообщении блога предлагается представлять перечисления с ключевыми словами в виде :type/value, поэтому ISQLValue можно реализовать следующим образом:

(defn kw->pgenum [kw]
  (let [type (-> (namespace kw)
                 (s/replace "-" "_"))
        value (name kw)]
    (doto (PGobject.)
      (.setType type)
      (.setValue value))))

(extend-type clojure.lang.Keyword
  jdbc/ISQLValue
  (sql-value [kw]
    (kw->pgenum kw)))

В вашем примере вы бы вставили свой продукт с:

(def prod-12345 {:sku "12345"
                 :name "My Product"
                 :description "yada yada yada"
                 :quantity 100
                 :cost 42.00
                 :price 59.00
                 :weight 0.3
                 ;; magic happens here
                 :status :product_status/InStock})

(sql/with-connection db-spec
   (sql/insert-record :product prod-12345))

Проблема заключается в том, что при запросе базы данных перечисление представляет собой простую строку вместо ключевого слова. Это можно решить аналогичным образом, реализовав протокол IResultSetReadColumn:

(def +schema-enums+
  "A set of all PostgreSQL enums in schema.sql. Used to convert
  enum-values back into Clojure keywords."
  ;; add your other enums here
  #{"product_status"})

(extend-type java.lang.String
  jdbc/IResultSetReadColumn
  (result-set-read-column [val rsmeta idx]
    (let [type (.getColumnTypeName rsmeta idx)]
      (if (contains? +schema-enums+ type)
        (keyword (s/replace type "_" "-") val)
        val))))