diff --git a/src/toucan/models.clj b/src/toucan/models.clj index 38b3814..669a7fb 100644 --- a/src/toucan/models.clj +++ b/src/toucan/models.clj @@ -266,7 +266,12 @@ (add-property! :timestamped? :insert (fn [obj] (assoc obj :created-at (new-timestamp), :updated-at (new-timestamp))) - :update (fn [obj] (assoc obj :updated-at (new-timestamp))))")) + :update (fn [obj] (assoc obj :updated-at (new-timestamp))))") + + (primary-key [this] + "Returns a keyword that represents the primary key. + + The default implementation returns `:id`")) ;;; INTERNAL IMPL @@ -337,19 +342,24 @@ :pre-update identity :post-select identity :pre-delete (constantly nil) - :hydration-keys (constantly nil)}) + :hydration-keys (constantly nil) + :primary-key (constantly :id)}) (defn- invoke-model "Fetch an object with a specific ID or all objects of type ENTITY from the DB. (invoke-model Database) -> seq of all databases - (invoke-model Database 1) -> Database w/ ID 1 + (invoke-model Database 1) -> Database w/ ID 1 (or primary-key) (invoke-model Database :id 1 ...) -> A single Database matching some key-value args" ([model] ((resolve 'toucan.db/select) model)) ([model id] (when id - (invoke-model model :id id))) + (if (map? id) + (let [args (vec (flatten (into [] id))) + invoke-part (partial invoke-model model)] + (apply invoke-part args)) + (invoke-model model (primary-key model) id)))) ([model k v & more] (apply (resolve 'toucan.db/select-one) model k v more))) diff --git a/test/toucan/models_test.clj b/test/toucan/models_test.clj index 82cb333..820fe52 100644 --- a/test/toucan/models_test.clj +++ b/test/toucan/models_test.clj @@ -4,6 +4,7 @@ [models :as models]) [toucan.test-setup :as test] (toucan.test-models [category :refer [Category], :as category] + [inventory :refer [Inventory map->InventoryInstance]] [user :refer [User]] [venue :refer [Venue map->VenueInstance]]) [toucan.util.test :as tu])) @@ -177,3 +178,13 @@ (expect #toucan.test_models.venue.VenueInstance{} (empty (Venue :name "BevMo"))) + +;; Test using a primary key other than `id` +(expect + #toucan.test_models.inventory.InventoryInstance{:name "Computer", :sku "cp-1" :id 1} + (Inventory "cp-1")) + +;; Test using multiple keys as a fake primary +(expect + #toucan.test_models.inventory.InventoryInstance{:name "Computer", :sku "cp-1" :id 1} + (Inventory {:sku "cp-1" :name "Computer"})) diff --git a/test/toucan/test_models/inventory.clj b/test/toucan/test_models/inventory.clj new file mode 100644 index 0000000..817c39e --- /dev/null +++ b/test/toucan/test_models/inventory.clj @@ -0,0 +1,9 @@ +(ns toucan.test-models.inventory + "A model with `:primary-key` set to `sku`" + (:require [toucan.models :as models])) + +(models/defmodel Inventory :inventories) + +(extend (class Inventory) + models/IModel + (merge models/IModelDefaults {:primary-key (constantly :sku)})) diff --git a/test/toucan/test_setup.clj b/test/toucan/test_setup.clj index 55b6bf7..61f0c46 100644 --- a/test/toucan/test_setup.clj +++ b/test/toucan/test_setup.clj @@ -5,6 +5,7 @@ (toucan [db :as db] [models :as models]) (toucan.test-models [category :refer [Category]] + [inventory :refer [Inventory]] [user :refer [User]] [venue :refer [Venue]]) [toucan.util.test :as u]) @@ -52,7 +53,13 @@ name VARCHAR(256) UNIQUE NOT NULL, \"parent-category-id\" INTEGER );" - "TRUNCATE TABLE categories RESTART IDENTITY CASCADE;")) + "TRUNCATE TABLE categories RESTART IDENTITY CASCADE;" + ;; Inventory + "CREATE TABLE IF NOT EXISTS inventories ( + id SERIAL PRIMARY KEY, + sku VARCHAR(256) UNIQUE NOT NULL, + name VARCHAR(256) NOT NULL);" + "TRUNCATE TABLE inventories RESTART IDENTITY CASCADE;")) (def ^java.sql.Timestamp jan-first-2017 (Timestamp/valueOf "2017-01-01 00:00:00")) @@ -72,7 +79,12 @@ [{:name "bar"} {:name "dive-bar", :parent-category-id 1} {:name "resturaunt"} - {:name "mexican-resturaunt", :parent-category-id 3}])) + {:name "mexican-resturaunt", :parent-category-id 3}]) + ;; Inventory + (db/simple-insert-many! Inventory + [{:name "Computer" :sku "cp-1"} + {:name "Text Book" :sku "tb-1"} + {:name "Pencils" :sku "pn-1"}])) (defn reset-db! "Reset the DB to its initial state, creating tables if needed and inserting the initial test data."