diff --git a/src/metabase/db.clj b/src/metabase/db.clj
index bb9602233d8f85e22342a824ce934d1a58c2f8cb..f760ae191c44d3703006773bd36c07be97318285 100644
--- a/src/metabase/db.clj
+++ b/src/metabase/db.clj
@@ -2,7 +2,8 @@
   "Korma database definition and helper functions for interacting with the database."
   (:require [clojure.java.jdbc :as jdbc]
             [clojure.tools.logging :as log]
-            [clojure.string :as str]
+            (clojure [set :as set]
+                     [string :as str])
             [environ.core :refer [env]]
             (korma [core :refer :all]
                    [db :refer :all])
@@ -131,63 +132,7 @@
     (apply setup-db args)))
 
 
-;; # UTILITY FUNCTIONS
-
-;; ## CAST-COLUMNS
-
-;; TODO - Doesn't Korma have similar `transformations` functionality? Investigate.
-
-(def ^:const ^:private type-fns
-  "A map of column type keywords to the functions that should be used to \"cast\"
-   them when going `:in` or `:out` of the database."
-  {:json    {:in  i/write-json
-             :out i/read-json}
-   :keyword {:in  name
-             :out keyword}})
-
-(defn types
-  "Tag columns in an entity definition with a type keyword.
-   This keyword will be used to automatically \"cast\" columns when they are present.
-
-    ;; apply ((type-fns :json) :in) -- cheshire/generate-string -- to value of :details before inserting into DB
-    ;; apply ((type-fns :json) :out) -- read-json -- to value of :details when reading from DB
-    (defentity Database
-      (types {:details :json}))"
-  [entity types-map]
-  {:pre [(every? keyword? (keys types-map))
-         (every? (partial contains? type-fns) (vals types-map))]}
-  (assoc entity ::types types-map))
-
-(defn apply-type-fns
-  "Recursively apply a sequence of functions associated with COLUMN-TYPE-PAIRS to OBJ.
-
-   COLUMN-TYPE-PAIRS should be the value of `(seq (::types korma-entity))`.
-   DIRECTION should be either `:in` or `:out`."
-  {:arglists '([direction column-type-pairs obj])}
-  [direction [[column column-type] & rest-pairs] obj]
-  (if-not column obj
-          (recur direction rest-pairs (if-not (column obj) obj
-                                              (update-in obj [column] (-> type-fns column-type direction))))))
-
-;; TODO - It would be good to allow custom types by just inserting the `{:in fn :out fn}` inline with the
-;; entity definition
-
-;; TODO - hydration-keys should be an entity function for the sake of prettiness
-
-
-;; ## TIMESTAMPED
-
-(defn timestamped
-  "Mark ENTITY as having `:created_at` *and* `:updated_at` fields.
-
-    (defentity Card
-      timestamped)
-
-   *  When a new object is created via `ins`, values for both fields will be generated.
-   *  When an object is updated via `upd`, `:updated_at` will be updated."
-  [entity]
-  (assoc entity ::timestamped true))
-
+;; # ---------------------------------------- UTILITY FUNCTIONS ----------------------------------------
 
 ;; ## UPD
 
@@ -202,10 +147,8 @@
   {:pre [(integer? entity-id)]}
   (let [obj (->> (assoc kwargs :id entity-id)
                  (models/pre-update entity)
-                 (#(dissoc % :id))
-                 (apply-type-fns :in (seq (::types entity))))
-        obj (cond-> obj
-              (::timestamped entity) (assoc :updated_at (u/new-sql-timestamp)))
+                 (models/internal-pre-update entity)
+                 (#(dissoc % :id)))
         result (-> (update entity (set-fields obj) (where {:id entity-id}))
                    (> 0))]
     (when result
@@ -335,7 +278,6 @@
 (defn -sel-transform [entity result]
   (->> result
        (models/internal-post-select entity)
-       #_(apply-type-fns :out (seq (::types entity)))
        (models/post-select entity)))
 
 (defmacro -sel-select
@@ -368,14 +310,10 @@
   [entity & {:as kwargs}]
   (let [vals (->> kwargs
                   (models/pre-insert entity)
-                  (apply-type-fns :in (seq (::types entity))))
-        vals (cond-> vals
-               (::timestamped entity) (assoc :created_at (u/new-sql-timestamp)
-                                             :updated_at (u/new-sql-timestamp)))
+                  (models/internal-pre-insert entity))
         {:keys [id]} (-> (insert entity (values vals))
-                         (clojure.set/rename-keys {(keyword "scope_identity()") :id}))]
-    (->> (sel :one entity :id id)
-         (models/post-insert entity))))
+                         (set/rename-keys {(keyword "scope_identity()") :id}))]
+    (models/post-insert entity (entity id))))
 
 
 ;; ## EXISTS?
diff --git a/src/metabase/models/card.clj b/src/metabase/models/card.clj
index 215cca55e01283634f46433d6495b85593496569..86e91a60aa380586bd6cd31d77e223c0d427679f 100644
--- a/src/metabase/models/card.clj
+++ b/src/metabase/models/card.clj
@@ -21,11 +21,9 @@
 
 (defentity Card
   [(table :report_card)
-   (types {:dataset_query          :json
-           :display                :keyword
-           :visualization_settings :json})
-   timestamped
-   (assoc :hydration-keys #{:card})]
+   (hydration-keys card)
+   (types :dataset_query :json, :display :keyword, :visualization_settings :json)
+   timestamped]
 
   (post-select [_ {:keys [creator_id] :as card}]
     (-> (assoc card
diff --git a/src/metabase/models/database.clj b/src/metabase/models/database.clj
index 03eb3b8c45b9943f95f3c012e893f90f0826d5b1..2709fdb72a261d6dcf3461c9dd718a61e8967ebd 100644
--- a/src/metabase/models/database.clj
+++ b/src/metabase/models/database.clj
@@ -19,11 +19,10 @@
 
 (defentity Database
   [(table :metabase_database)
-   (types {:details :json
-           :engine  :keyword})
-   timestamped
-   (assoc :hydration-keys #{:database
-                            :db})]
+   (hydration-keys database db)
+   (types :details :json, :engine :keyword)
+   timestamped]
+
   (post-select [_ db]
     (map->DatabaseInstance
      (assoc db
diff --git a/src/metabase/models/field.clj b/src/metabase/models/field.clj
index a6495b5eea4f21e723c23cd8acfdaf8367167fbb..9417e5a2d98205f2bf4223cd383de7f4d6f1defe 100644
--- a/src/metabase/models/field.clj
+++ b/src/metabase/models/field.clj
@@ -76,11 +76,9 @@
 
 (defentity Field
   [(table :metabase_field)
-   timestamped
-   (types {:base_type    :keyword
-           :field_type   :keyword
-           :special_type :keyword})
-   (hydration-keys destination field origin)]
+   (hydration-keys destination field origin)
+   (types :base_type :keyword, :field_type :keyword, :special_type :keyword)
+   timestamped]
 
   (pre-insert [_ field]
     (let [defaults {:active          true
diff --git a/src/metabase/models/field_values.clj b/src/metabase/models/field_values.clj
index 9537c8068d314d243f342c8311a2488782389ed0..e6c25f51ea83668e05a8ef0e07a98901d7dcdbe2 100644
--- a/src/metabase/models/field_values.clj
+++ b/src/metabase/models/field_values.clj
@@ -10,8 +10,7 @@
 (defentity FieldValues
   [(table :metabase_fieldvalues)
    timestamped
-   (types {:human_readable_values :json
-           :values                :json})]
+   (types :human_readable_values :json, :values :json)]
 
   (post-select [_ field-values]
     (update-in field-values [:human_readable_values] #(or % {}))))
diff --git a/src/metabase/models/foreign_key.clj b/src/metabase/models/foreign_key.clj
index 4737681b5bc3595647467c8b58beb3657055757a..05b4ad25446fc001686e5831b80102b0e746e6a6 100644
--- a/src/metabase/models/foreign_key.clj
+++ b/src/metabase/models/foreign_key.clj
@@ -16,8 +16,8 @@
 
 (defentity ForeignKey
   [(table :metabase_foreignkey)
-   timestamped
-   (types {:relationship :keyword})]
+   (types :relationship :keyword)
+   timestamped]
 
   (post-select [_ {:keys [origin_id destination_id] :as fk}]
     (assoc fk
diff --git a/src/metabase/models/interface.clj b/src/metabase/models/interface.clj
index 18f36b499db7c655b55760e27901d80a674cb1e4..c6300681e0371f96b1a2cdf74c2a598c96927526 100644
--- a/src/metabase/models/interface.clj
+++ b/src/metabase/models/interface.clj
@@ -4,7 +4,8 @@
             [clojure.walk :refer [macroexpand-all]]
             [korma.core :as k]
             [medley.core :as m]
-            [metabase.config :as config]))
+            [metabase.config :as config]
+            [metabase.util :as u]))
 
 ;;; ## ---------------------------------------- ENTITIES ----------------------------------------
 
@@ -33,8 +34,6 @@
 
      The output of this function is ignored.")
 
-  (internal-post-select [this instance])
-
   (post-select [this instance]
     "Called on the results from a call to `sel`. Default implementation doesn't do anything, but
      you can provide custom implementations to do things like add hydrateable keys or remove sensitive fields.")
@@ -48,21 +47,22 @@
 
         (pre-cascade-delete [_ {database-id :id :as database}]
           (cascade-delete Card :database_id database-id)
-          ...)"))
+          ...)")
+
+  (internal-pre-insert  [this instance])
+  (internal-pre-update  [this instance])
+  (internal-post-select [this instance]))
 
-(defn- identity-second
-  "Return the second arg as-is."
-  [_ obj]
-  obj)
+(defn- identity-second [_ obj] obj)
+(def ^:private constantly-nil (constantly nil))
 
 (def ^:const ^:private default-entity-method-implementations
-  {:pre-insert           identity-second
-   :post-insert          identity-second
-   :pre-update           identity-second
-   :post-update          '(constantly nil)
-   :post-select          identity-second
-   :pre-cascade-delete   '(constantly nil)
-   :internal-post-select identity-second})
+  {:pre-insert           #'identity-second
+   :post-insert          #'identity-second
+   :pre-update           #'identity-second
+   :post-update          #'constantly-nil
+   :post-select          #'identity-second
+   :pre-cascade-delete   #'constantly-nil})
 
 (def ^:const ^:private type-fns
   {:json    {:in  'metabase.db.internal/write-json
@@ -70,8 +70,16 @@
    :keyword {:in  `name
              :out `keyword}})
 
-(defn- resolve-type-fns [types-map]
-  (m/map-vals #(:out (type-fns %)) types-map))
+(defmacro apply-type-fns [obj-binding direction entity-map]
+  {:pre [(symbol? obj-binding)
+         (keyword? direction)
+         (map? entity-map)]}
+  (let [fns (m/map-vals #(direction (type-fns %)) (::types entity-map))]
+    (if-not (seq fns) obj-binding
+            `(cond-> ~obj-binding
+               ~@(mapcat (fn [[k f]]
+                           [`(~k ~obj-binding) `(update-in [~k] ~f)])
+                         fns)))))
 
 (defn -invoke-entity [entity id]
   (future
@@ -86,15 +94,18 @@
            (internal-post-select entity)
            (post-select entity)))))
 
-(defmacro make-internal-post-select [obj kvs]
-  `(cond-> ~obj
-     ~@(mapcat (fn [[k f]]
-                 [`(~k ~obj) `(update-in [~k] ~f)])
-               (seq kvs))))
+(defn- update-updated-at [obj]
+  (assoc obj :updated_at (u/new-sql-timestamp)))
+
+(defn- update-created-at-updated-at [obj]
+  (let [ts (u/new-sql-timestamp)]
+    (assoc obj :created_at ts, :updated_at ts)))
 
 (defmacro macrolet-entity-map [entity & entity-forms]
-  `(macrolet [(~'default-fields [m# & fields#] `(assoc ~m# ::default-fields [~@(map keyword fields#)]))
-              (~'hydration-keys [m# & fields#] `(assoc ~m# :hydration-keys #{~@(map keyword fields#)}))]
+  `(macrolet [(~'default-fields [m# & fields#]       `(assoc ~m# ::default-fields [~@(map keyword fields#)]))
+              (~'timestamped    [m#]                 `(assoc ~m# ::timestamped true))
+              (~'types          [m# & {:as fields#}] `(assoc ~m# ::types ~fields#))
+              (~'hydration-keys [m# & fields#]       `(assoc ~m# :hydration-keys #{~@(map keyword fields#)}))]
      (-> (k/create-entity ~(name entity))
          ~@entity-forms)))
 
@@ -105,8 +116,7 @@
          (every? list? methods)]}
   (let [entity-symb               (symbol (format "%sEntity" (name entity)))
         internal-post-select-symb (symbol (format "internal-post-select-%s" (name entity)))
-        entity-map                (eval `(macrolet-entity-map ~entity ~@entity-forms))
-        type-fns                  (resolve-type-fns (:metabase.db/types entity-map))]
+        entity-map                (eval `(macrolet-entity-map ~entity ~@entity-forms))]
     `(do
        (defrecord ~entity-symb []
          clojure.lang.IFn
@@ -115,13 +125,20 @@
 
        (extend ~entity-symb
          IEntity ~(merge default-entity-method-implementations
-                         {:internal-post-select `(fn [~'_ ~'obj]
-                                                  ~(macroexpand-1 `(make-internal-post-select ~'obj ~type-fns)))}
+                         {:internal-pre-insert  `(fn [~'_ obj#]
+                                                   (-> (apply-type-fns obj# :in ~entity-map)
+                                                       ~@(when (::timestamped entity-map)
+                                                           [update-created-at-updated-at])))
+                          :internal-pre-update  `(fn [~'_ obj#]
+                                                   (-> (apply-type-fns obj# :in ~entity-map)
+                                                       ~@(when (::timestamped entity-map)
+                                                           [update-updated-at])))
+                          :internal-post-select `(fn [~'_ obj#]
+                                                   (apply-type-fns obj# :out ~entity-map))}
                          (into {}
                                (for [[method-name & impl] methods]
                                  {(keyword method-name) `(fn ~@impl)}))))
-
-       (def ~entity            ; ~(vary-meta entity assoc :const true)
+       (def ~entity
          (~(symbol (format "map->%sEntity" (name entity))) ~entity-map)))))
 
 
diff --git a/src/metabase/models/query_execution.clj b/src/metabase/models/query_execution.clj
index 99d408e0ac8dd3fbb75c4a5eb6393cd4f8c8473b..0599fa0977c9691e97f5c7ddf111f1d739d3d6a5 100644
--- a/src/metabase/models/query_execution.clj
+++ b/src/metabase/models/query_execution.clj
@@ -10,9 +10,7 @@
 (defentity QueryExecution
   [(table :query_queryexecution)
    (default-fields id uuid version json_query raw_query status started_at finished_at running_time error result_rows)
-   (types {:json_query  :json
-           :result_data :json
-           :status      :keyword})]
+   (types :json_query :json, :result_data :json, :status :keyword)]
 
   (post-select [_ {:keys [result_rows] :as query-execution}]
     ;; sadly we have 2 ways to reference the row count :(
diff --git a/src/metabase/models/table.clj b/src/metabase/models/table.clj
index a550536a7eee777ebe2a91d81681d2f530af6b75..8978266fdce1ddeb01cfa7646fbc7995058caf0d 100644
--- a/src/metabase/models/table.clj
+++ b/src/metabase/models/table.clj
@@ -14,9 +14,9 @@
 
 (defentity Table
   [(table :metabase_table)
-   timestamped
    (hydration-keys table)
-   (types {:entity_type :keyword})]
+   (types :entity_type :keyword)
+   timestamped]
 
   (post-select [_ {:keys [id db db_id description] :as table}]
     (u/assoc* table