简体   繁体   English

通过 clojure.java.jdbc 在 Clojure 中使用外键约束

[英]Using foreign key constraints in Clojure with clojure.java.jdbc

I'm working on a wiki program and using SQLite as the database.我正在开发一个 wiki 程序并使用 SQLite 作为数据库。 I want to create a many-to-many relationship between wiki pages and tags describing those pages.我想在 wiki 页面和描述这些页面的标签之间创建多对多关系。 I'm using clojure.java.jdbc to handle the database operations.我使用clojure.java.jdbc来处理数据库操作。 I would like to enforce foreign key constraints in the page-to-tags cross-reference table.我想在页面到标签交叉引用表中强制执行外键约束。 I looked at the information about foreign keys on the SQLite site ( https://www.sqlite.org/foreignkeys.html ) and believe something like this is what I want;我查看了 SQLite 站点 ( https://www.sqlite.org/foreignkeys.html ) 上有关外键的信息,并相信这样的事情就是我想要的;

(def the-db-name "the.db")
(def the-db {:classname   "org.sqlite.JDBC"
             :subprotocol "sqlite"
             :subname     the-db-name})

(defn create-some-tables
  "Create some tables and a cross-reference table with foreign key constraints."
  []
  (try (jdbc/db-do-commands
         the-db false
         ["PRAGMA foreign_keys = ON;"
          (jdbc/create-table-ddl :pages
                                 [[:page_id :integer :primary :key]
                                  ;...
                                  [:page_content :text]])
          (jdbc/create-table-ddl :tags
                                 [[:tag_id :integer :primary :key]
                                  [:tag_name :text "NOT NULL"]])
          (jdbc/create-table-ddl :tags_x_pages
                                 [[:x_ref_id :integer :primary :key]
                                  [:tag_id :integer]
                                  [:page_id :integer]
                                  ["FOREIGN KEY(tag_id) REFERENCES tags(tag_id)"]
                                  ["FOREIGN KEY(page_id) REFERENCES pages(page_id)"]])])

       (catch Exception e (println e))))

But attempting to turn the pragma on has no effect.但是尝试打开 pragma 没有效果。

Just trying to turn the pragma on and check for effect:只是试图打开编译指示并检查效果:

(println "Check before:" (jdbc/query the-db ["PRAGMA foreign_keys;"]))
; Transactions on or off makes no difference.
(println "Result of execute!:" (jdbc/execute! the-db
                                              ["PRAGMA foreign_keys = ON;"]))
(println "Check after:" (jdbc/query the-db ["PRAGMA foreign_keys;"]))

;=> Check before: ({:foreign_keys 0})
;=> Result of execute!: [0]
;=> Check after: ({:foreign_keys 0})

The results indicate that the library (org.xerial/sqlite-jdbc "3.21.0.1") was compiled to support foreign keys since there were no errors, but trying to set the pragma has no effect.结果表明库 (org.xerial/sqlite-jdbc "3.21.0.1") 被编译为支持外键,因为没有错误,但尝试设置编译指示没有效果。

I found this in the JIRA for the clojure JDBC back in 2012. The described changes have been implemented since then, but the code still has no effect.早在 2012 年,我就在 Clojure JDBC 的 JIRA 中发现了这一点。从那时起已实现了所描述的更改,但代码仍然没有效果。

Finally found this answer to a Stackoverflow question that pointed to this post back in 2011. That allowed me to cobble together something that did seem to set the pragma.终于在 2011 年找到了指向这篇文章的 Stackoverflow 问题的答案。这让我能够拼凑出一些似乎确实设定了 pragma 的东西。 The code below depends on creating a specially configured Connection .下面的代码取决于创建一个专门配置的Connection

(ns example
  (:require [clojure.java.jdbc :as jdbc])
  (:import (java.sql Connection DriverManager)
           (org.sqlite SQLiteConfig)))

(def the-db-name "the.db")
(def the-db {:classname   "org.sqlite.JDBC"
             :subprotocol "sqlite"
             :subname     the-db-name})

(defn ^Connection get-connection
  "Return a connection to a SQLite database that
  enforces foreign key constraints."
  [db]
  (Class/forName (:classname db))
  (let [config (SQLiteConfig.)]
    (.enforceForeignKeys config true)
    (let [connection (DriverManager/getConnection
                       (str "jdbc:sqlite:" (:subname db))
                       (.toProperties config))]
      connection)))

(defn exec-foreign-keys-pragma-statement
  [db]
  (let [con ^Connection (get-connection db)
        statement (.createStatement con)]
    (println "exec-foreign-keys-pragma-statement:"
             (.execute statement "PRAGMA foreign_keys;"))))

Based on the above, I rewrote the table creation code above as:基于以上,我将上面的建表代码改写为:

(defn create-some-tables
  "Create some tables and a cross-reference table with foreign key constraints."
  []
  (when-let [conn (get-connection the-db)]
    (try
      (jdbc/with-db-connection
        [conn the-db]
        ; Creating the tables with the foreign key constraints works.
        (try (jdbc/db-do-commands
               the-db false
               [(jdbc/create-table-ddl :pages
                                       [[:page_id :integer :primary :key]
                                        [:page_content :text]])
                (jdbc/create-table-ddl :tags
                                       [[:tag_id :integer :primary :key]
                                        [:tag_name :text "NOT NULL"]])
                (jdbc/create-table-ddl :tags_x_pages
                                       [[:x_ref_id :integer :primary :key]
                                        [:tag_id :integer]
                                        [:page_id :integer]
                                        ["FOREIGN KEY(tag_id) REFERENCES tags(tag_id)"]
                                        ["FOREIGN KEY(page_id) REFERENCES pages(page_id)"]])])

             ; This still doesn't work.
             (println "After table creation:"
                      (jdbc/query the-db "PRAGMA foreign_keys;"))

             (catch Exception e (println e))))

      ; This returns the expected results.
      (when-let [statement (.createStatement conn)]
        (try
          (println "After creating some tables: PRAGMA foreign_keys =>"
                   (.execute statement "PRAGMA foreign_keys;"))
          (catch Exception e (println e))
          (finally (when statement
                     (.close statement)))))
      (catch Exception e (println e))
      (finally (when conn
                 (.close conn))))))

The tables are created as expected.表按预期创建。 Some of the clojure.java.jdbc functions still don't seem to work as desired though.但是,某些clojure.java.jdbc函数似乎仍然无法按预期工作。 (See the jdbc/query call in the middle of the listing.) Getting things to always work as expected seems very "manual" having to fall back on java interop. (请参阅清单中间的jdbc/query调用。)让事情始终按预期工作似乎非常“手动”,不得不依靠 Java 互操作。 And it seems like every interaction with the database requires using the specially configured Connection returned by the get-connection function.似乎每次与数据库的交互都需要使用由get-connection函数返回的专门配置的Connection

Is there a better way to enforce foreign key constraints in SQLite in Clojure?有没有更好的方法在 Clojure 中的 SQLite 中强制执行外键约束?

I've not played with SqlLite, but would recommend you test with either我没有玩过 SqlLite,但建议你测试

Also, when debugging it may be easier to use pure SQL strings (see http://clojure-doc.org/articles/ecosystem/java_jdbc/using_sql.html ):此外,在调试时,使用纯 SQL 字符串可能更容易(参见http://clojure-doc.org/articles/ecosystem/java_jdbc/using_sql.html ):

(j/execute! db-spec
            ["update fruit set cost = ( 2 * grade ) where grade > ?" 50.0])

Using pure SQL strings (especially when debugging) can avoid many misunderstandings/pitfalls with JDBC.使用纯 SQL 字符串(尤其是在调试时)可以避免对 JDBC 的许多误解/陷阱。 Also, keep in mind that you may discover a bug in either the Clojure JDBC libs or the DB itself.另外,请记住,您可能会在 Clojure JDBC 库或数据库本身中发现错误。

I'm not sure SQLite does support the features you described above.我不确定 SQLite 是否支持您上面描述的功能。 If you really want to keep your data being consisted with strict constraints, use PostgeSQL database.如果你真的想让你的数据受到严格的约束,请使用 PostgeSQL 数据库。 I know that working with SQLite seems easier especially when you've just started the project, but believe me, using Postgres really worth it.我知道使用 SQLite 似乎更容易,尤其是当您刚开始项目时,但相信我,使用 Postgres 真的很值得。

Here is an example of post and tags declaration using Postgres that takes lots of details into account:下面是一个使用 Postgres 的 post 和 tags 声明示例,它考虑了很多细节:

create table post(
  id serial primary key,
  title text not null,
  body text not null
);

create table tags(
  id serial primary key,
  text text not null unique
);

create table post_tags(
  id serial primary key,
  post_id integer not null references posts(id),
  tag_id integer not null references tags(id),
  unique(post_id, tag_id)
);

Here, the tags table cannot contain two equal tags.在这里, tags表不能包含两个相等的标签。 That's important to keep only unique tag strings to prevent the table from growing.仅保留唯一的标记字符串以防止表增长很重要。

The bridge table that links a post with tags has a special constraint to prevent a case when a specific tag is linked to a post several times.链接帖子与标签的桥表有一个特殊的约束,以防止特定标签多次链接到帖子的情况。 Say, if a post has "python" and "clojure" tags attached, you won't be able to add "python" one more time.比如说,如果帖子附加了“python”和“clojure”标签,您将无法再添加一次“python”。

Finally, each reference clause when declaring a table creates a special constraint that prevents you from referencing an id that does not exist in a target table.最后,声明表时的每个reference子句都会创建一个特殊约束,以防止您引用目标表中不存在的 id。

Installing Postgres and setting it up might be a bit difficult, but nowadays there are such one-click applications like Postgres App that are quite easy to use even if you are not familiar with them.安装和设置 Postgres 可能有点困难,但是现在有像Postgres App这样的一键式应用程序,即使您不熟悉它们也很容易使用。

With the advent of next.jdbc you can now do that like so:随着next.jdbc的出现,您现在可以这样做:

(ns dev
  (:require [next.jdbc :as jdbc]
            [next.jdbc.sql :as sql]))

(with-open [conn (jdbc/get-connection {:dbtype "sqlite" :dbname "test.db"})]
  (println (sql/query conn ["PRAGMA foreign_keys"]))
  (jdbc/execute! conn ["PRAGMA foreign_keys = ON"])
  ; jdbc/execute whatever you like here...
  (println (sql/query conn ["PRAGMA foreign_keys"])))

This outputs这输出

[{:foreign_keys 0}]
[{:foreign_keys 1}]

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM