Skip to content
This repository

Clojure API for generic persistence.

branch: master

3.7.1

latest commit b1b8f60a5e
Micah Martin slagyr authored September 26, 2013
Octocat-spinner-32 api 3.7.1 September 26, 2013
Octocat-spinner-32 gae 3.7.1 September 26, 2013
Octocat-spinner-32 mongo 3.7.1 September 26, 2013
Octocat-spinner-32 mysql 3.7.1 September 26, 2013
Octocat-spinner-32 postgres 3.7.1 September 26, 2013
Octocat-spinner-32 redis 3.7.1 September 26, 2013
Octocat-spinner-32 riak 3.7.1 September 26, 2013
Octocat-spinner-32 sql 3.7.1 September 26, 2013
Octocat-spinner-32 sqlite 3.7.1 September 26, 2013
Octocat-spinner-32 .gitignore build tweak September 25, 2013
Octocat-spinner-32 .travis.yml fix travis December 28, 2012
Octocat-spinner-32 CHANGES.md 3.7.1 September 26, 2013
Octocat-spinner-32 DEVELOPMENT.md updates todo list; bumps version in the README December 09, 2012
Octocat-spinner-32 README.md Fix typo in readme September 23, 2013
Octocat-spinner-32 Rakefile build tweak September 25, 2013
Octocat-spinner-32 TODO.md adds spec for characters January 08, 2013
Octocat-spinner-32 hyperion_logo.png adding logo and start to log files September 01, 2012
README.md

Hyperion logo

Hyperion Build Status

1 API, multiple database backends.

Hyperion provides you with a simple API for data persistence allowing you to delay the choice of database without delaying your development.

There are a few guiding principles for Hyperion.

  1. key/value store. All Hyperion implementations, even for relational databases, conform to the simple key/value store API.
  2. values are maps. Every 'value' that goes in or out of a Hyperion datastore is a map.
  3. :key and :kind. Every 'value' must have a :kind entry; a short string like "user" or "product". Persisted 'value's will have a :key entry; strings generated by the datastore.
  4. Search with data. All searches are described by data. See find-by-kind below.

Hyperion Implementations:

Installation

Leiningen

:dependencies [[hyperion/hyperion-<impl here> "3.7.0"]]

Usage

Creating a datastore

hyperion.api provides a convenient factory function for instantiating any datastore implementation

(use 'hyperion.api)
(new-datastore :implementation :memory)
(new-datastore :implementation :mysql :connection-url "jdbc:mysql://localhost:3306/myapp?user=root" :database "myapp")
(new-datastore :implementation :mongo :host "localhost" :port 27017 :database "myapp" :username "test" :password "test")

Each implementation provides their own facilities of course:

(use 'hyperion.mongo)
(new-mongo-datastore :host "localhost" :port 27017 :database "myapp" :username "test" :password "test")
;or
(let [mongo (open-mongo :host "127.0.0.1" :port 27017)
      db (open-database mongo "myapp" :username "test" :password "test")]
   (new-mongo-datastore db))

Installing a datastore

; with brute force
(set-ds! (new-datastore ...))
; with elegance
(binding [*ds* (new-datastore ...)]
  ; persistence stuff here)

Ideally, bind the datastore once at a high level in your application, if you can. Otherwise use the brute force set-ds! technique.

Saving a value:

(save {:kind :foo})
;=> {:kind "foo" :key "generated key"}
(save {:kind :foo} {:value :bar})
;=> {:kind "foo" :value :bar :key "generated key"}
(save {:kind :foo} :value :bar)
;=> {:kind "foo" :value :bar :key "generated key"}
(save {:kind :foo} {:value :bar} :another :fizz)
;=> {:kind "foo" :value :bar :another :fizz :key "generated key"}
(save (citizen) :name "Joe" :age 21 :country "France")
;=> #<{:kind "citizen" :name "Joe" :age 21 :country "France" ...}>

Updating a value:

(let [record (save {:kind :foo :name "Sue"})
      new-record (assoc record :name "John")]
  (save new-record))
;=> {:kind "foo" :name "John" :key "generated key"}

Loading a value:

; if you have a key...
(find-by-key my-key)

; otherwise
(find-by-kind :dog) ; returns all records with :kind of "dog"
(find-by-kind :dog :filters [:= :name "Fido"]) ; returns all dogs whos name is Fido
(find-by-kind :dog :filters [[:> :age 2][:< :age 5]]) ; returns all dogs between the age of 2 and 5 (exclusive)
(find-by-kind :dog :sorts [:name :asc]) ; returns all dogs in alphebetical order of their name
(find-by-kind :dog :sorts [[:age :desc][:name :asc]]) ; returns all dogs ordered from oldest to youngest, and dogs of the same age ordered by name
(find-by-kind :dog :limit 10) ; returns upto 10 dogs in undefined order
(find-by-kind :dog :sorts [:name :asc] :limit 10) ; returns up to the first 10 dogs in alphebetical order of their name
(find-by-kind :dog :sorts [:name :asc] :limit 10 :offset 10) ; returns the second set of 10 dogs in alphebetical order of their name

Deleting a value:

; if you have a key...
(delete-by-key my-key)

; otherwise
(delete-by-kind :dog) ; deletes all records with :kind of "dog"
(delete-by-kind :dog :filters [:= :name "Fido"]) ; deletes all dogs whos name is Fido
(delete-by-kind :dog :filters [[:> :age 2][:< :age 5]]) ; deletes all dogs between the age of 2 and 5 (exclusive)

Filters and Sorts

Filter operations and acceptable syntax:

  • := "=" "eq"
  • :< "<" "lt"
  • :<= "<=" "lte"
  • :> ">" "gt"
  • :>= ">=" "gte"
  • :!= "!=" "not"
  • :contains? "contains?" :contains "contains" :in? "in?" :in "in"

Sort orders and acceptable syntax:

  • :asc "asc" :ascending "ascending"
  • :desc "desc" :descending "descending"

The :filter and :sort options are usable in find-by-kind, find-by-all-kinds, and delete-by-kind. The :limit option may also be used in the find-by- functions.

Note: Filters and Sorts on :key are not supported. Some datastore implementations don't store the :key along with the data, so you can't very well filter or sort something that aint there.

Entities

Used to define entities. An entity is simply an encapsulation of data that is persisted. The advantage of using entities are:

  • they limit the fields persisted to only what is specified in their definition.
  • default values can be assigned to fields.
  • types, packers, and unpackers can be assigned to fields. Packers allow you to manipulate a field (perhaps serialize it) before it is persisted. Unpacker conversly manipulate fields when loaded. Packers and unpackers may be a fn (which will be excuted) or an object used to pivot the pack and unpack multimethods. A type (object) is simply a combined packer and unpacker.
  • constructors are provided.

Example:

(use 'hyperion.types)

(defentity Citizen
    [name]
    [age :packer ->int] ; ->int is a function defined in your code.
    [gender :unpacker ->string] ; ->string is a customer function too.
    [occupation :type my.ns.Occupation] ; and then we define pack/unpack for my.ns.Occupation
    [spouse-key :type (foreign-key :citizen)] ; :key is a special type that pack string keys into implementation-specific keys
    [country :default "USA"] ; newly created records will use the default if no value is provided
    [created-at] ; populated automaticaly
    [updated-at] ; also populated automatically
    )

(save (citizen :name "John" :age "21" :gender :male :occupation coder :spouse-key "abc123"))

;=> #<{:kind "citizen" :key "some generated key" :country "USA" :created-at #<java.util.Date just-now> :updated-at #<java.util.Date just-now> ...)

Foreign Keys

In a traditional SQL database, you may have a schema that looks like this:

users:

  • id
  • first_name
  • created_at
  • updated_at

profiles:

  • id
  • user_id
  • created_at
  • updated_at

Since Hyperion presents every underlying datastore as a key-value store, configuring Hyperion to use this schema is a little tricky, but certainly possible.

This is what the coresponding defentity notation would be:

(use 'hyperion.api)
(use 'hyperion.types)

(defentity :users
  [first-name]
  [created-at]
  [updated-at]
  )

(defentity :profiles
  [user-key :type (foreign-key :users) :db-name :user-id]
  [created-at]
  [updated-at]
  )

(let [myles (save {:kind :users :first-name "Myles"})
      myles-profile (save {:kind :profiles :user-key (:key myles)})]
; myles => {:key "b26316a0248244bab65c699778897ab9", :created-at #inst "2012-12-05T15:41:23.589-00:00", :updated-at #inst "2012-12-05T15:41:23.589-00:00", :first-name "Myles", :kind "users"}
; myles is stored in the users table as:
; | id | first_name | created_at | updated_at |
; | 1  | Myles      | <time>     | <time>     |

; myles-profile => {:key "7202968b5ecf47aab686990750a3238a", :user-key "b26316a0248244bab65c699778897ab9", :created-at #inst "2012-12-05T15:43:16.529-00:00", :updated-at #inst "2012-12-05T15:43:16.529-00:00", :kind "profiles"}
; myles' profile is stored in the profiles table as:
; | id | user_id | created_at | updated_at |
; | 1  | 1       | <time>     | <time>     |

  (= (find-by-key (:user-key myles-profile)) myles) ;=> true
  )

Using the foreign-key type, our foreign key references are stored following the conventions of the underlying datastore. In this example, the user-key field will be packed as an integer id, as stored in the user-id column.

If your schema requires foreign keys, ALWAYS USE THE FOREIGN KEY TYPE. If you do not, you will be storing generated keys instead of actual database ids. DO NOT DO THIS. If Hyperion changes the way it generates keys, all of your foreign key data will be useless.

Types

All hyperion implementations provide built-in support for the following types:

  • java.lang.Boolean
  • java.lang.Byte
  • java.lang.Short
  • java.lang.Integer
  • java.lang.Long
  • java.lang.Float
  • java.lang.Double
  • java.lang.Character
  • java.lang.String
  • clojure.lang.Keyword

Implementations may either support the type Natively or with a packer/unpacker. If they are natively supported, no configuration is needed. If supported by a packer/unpacker, you must explicitly configure the type. For example:

(defentity :users
  [first-name :type java.lang.String]
  [age :type java.lang.Integer])

It is always best to explicitly state the types of all fields, regardless of implementation, so that you don't have to worry about the differences between datastores.

Unsupported Types

The following types do not have built-in support:

  • java.math.BigInteger
  • java.math.BigDecimal

There are many different opinions on the best way to store these types. We will leave it up to you to store them in the way that you see fit.

Logging

Many of the Hyperion components will log informative information (more logging has yet to be added). The default log level is Info. Not much is logged at the info level. To get more informative log message, turn on the Debug log level.

(hyperion.log/debug!)

You can also log your own messages.

(hyperion.log/debug "This is a debug message")
(hyperion.log/info "Hey, here's some info!")

The complete list of log levels (which come from timbre) are [:trace :debug :info :warn :error :fatal :report].

Full API

To learn more, download hyperion.api and load up the REPL.

user=> (keys (ns-publics 'hyperion.api))
(delete-by-key save* count-all-kinds save find-by-key reload pack create-entity-with-defaults delete-by-kind defentity *ds*
before-save find-by-kind count-by-kind after-load after-create new-datastore ds unpack create-entity set-ds! find-all-kinds new?)

user=> (doc delete-by-key)
-------------------------
hyperion.api/delete-by-key
([key])
  Removes the record stored with the given key.
  Returns nil no matter what.
Something went wrong with that request. Please try again.