Skip to content

amcnamara/slurm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

59 Commits
 
 
 
 
 
 

Repository files navigation

DEPRECATION NOTICE: Slurm is no longer maintained, and hasn't been updated to modern versions of Clojure. This repository is a proof of concept for some interesting ideas, but these should not be considered idiomatic for either Clojure or MySQL.

SLURM

Slurm is a Clojure ORM. Its modelled after a traditional Object-Oriented ORM but also includes a dynamic DSL for read/write operations. It supports the following features:

  • Records can be queried and processed as sequences, with eager- or lazy-loading
  • Records can be mapped with single and multi-object relations
  • Homoiconic schemas, easy to adapt to existing projects/DBs
  • Macro-based read/write functions which symbolically map to schema tables

Overview

The first thing you need to do is define a schema for your DB; this includes credentials and connection information, as well as definitions of all tables and columns. The schema is defined in a standard hash as follows:

{ :db-host  <ip-address>   (default: localhost)
  :db-port  <port-number>  (default: 3306)
  :db-user  <username>     (default: root)
  :db-pass  <password>     (default: n/a)
  :loading  eager|lazy     (default: lazy)
  :tables   #{<tables>} }

The root-level of the db-schema map concerns db credentials, the value of :tables is a collection of table definitions described as follows:

{ :name                        <table-name>
  :primary-key                 <key-name>      (default: :id)
  :primary-key-type            <type>          (default: "int(11)")
  :primary-key-auto-increment  true|false      (default: true)
  :fields                      {<field-map} }

Now that you've defined the table outlines, you need to fill in the fields that the table will hold, as follows:

{ <field-name> <field-type>, ... }

Note that all field names (and table names) should be keywordized. Field types can either be SQL primatives (described below), or references to other tables. Single-reference fields (ie. the field will point to at most a single row in the foreign table) are defined by putting the keyword table name of the relational table into the field-type value. Multi-reference fields (ie. one which contains reference to many rows of a foreign table) are defined with the keyword table name prefixed with an asterisk, so a field called :enrolled_students with a multi-record relation to a table named :student may be defined as follows:

{ :enrolled_students :*student, ... }

Fields which represent standard SQL types may have the following field-types:

"int(n)"
"varchar(n)"
"bool"
"date"
"datetime"
"blob"

SEE: MySQL documentation for further details on supported native types.

A complete example of a schema definition can be found under src/example/db_schema, it may help clarify some of the naming conventions described here. To see it in use launch the script under src/example/db_example.clj with the instructions given below.

Interaction

Now that you've got your schema defined you're ready to use slurm! Run lein jar from the slurm source folder and move the resulting .jar into your project's lib folder.

Interacting with your database is extremely easy and can all be done from within the with-orm macro, which will take a db-schema and a body of code and evaluate that code with some db helpers. The macro will dynamically bind symbols within the body that you can use to interact with the db, each of the tables defined in the db-schema will have their own read/write functions. For the following examples we assume that the db-schema passed into with-orm has defined a table named :employee.

(defn my-program [my-db-schema]
  (with-orm my-db-schema
    (let [new-employee (employee! { :name "Alex McNamara", team: "Engineering" })])))

This will create a new employee record in the database. The object returned from this function is called a DBObject (DBO), and they're used to hold the state of a particular db row (in this case a :employee table's row, with the :name field value of "Alex McNamara" and :team field value of Engineering). Creating data is that easy, and one of these helpers will be bound for each table definition in the db-schema with via transformation of :tablename to tablename!, and accept a map of field:value pairs to persist a row.

The DBO record's individual column fields can be inspected via the field operation:

(field new-employee :name) ;; returns "Alex McNamara"

Queries can be executed via the dynamically-injected <table-name> operation, the select helpers would be defined in the above example as (employee <primary-key>), or (employee <field-name> <field-value>). Genereally, the helpers follow the transformation of :tablename to tablename, and accept single, double, or variadic args as shown above; building on the previous example:

(let [ new-employee-team-members (employee :team (field new-employee :team)) ])

This would grab a sequence of all employee records with the same :team field value as the new-employee DBO, and return (at least) the one defined in the exercise above. Passing a single argument to employee would query on that table's primary key, so (employee 13) would grab the row with :id of 13, and return the singleton DBO.

Mutation can be accomplished via the assoc* operation, which takes a DBO and a map of a subset of the record's field:value pairs and modifies the corresponding row in the database. It returns a new DBO representing the updated state of the record. Building on the example above we can change the :name field of the existing new-employee record:

(assoc* new-employee { :name "Alexander McNamara" })

Lastly, the delete operatino takes a DBO and removes the corresponding row from the database:

(delete new-employee)

That's it, these four operations should cover the trivial use cases for dealing with DB records. The current state of query support only enables PK and single-conditional queries; intersection and union conditionals were never implemented. Writes (especially for related records) should not be considered atomic transactions, and thus not concurrently safe. There are lots of notes in the code describing potential improvements as well as areas where the implementation compromised robustness in favor of a simplified object data interface.

Examples

There is some example code under src/example/db_example.clj, which can simply be run via:

lein compile
lein run

About

An experimental Clojure ORM.

Topics

Resources

Stars

Watchers

Forks