@abw/badger-database
Version:
Javascript database abstraction layer
405 lines (336 loc) • 13.2 kB
Markdown
# Badger Database
This is a simple but powerful database management tool that allows you to
build database abstraction layers for your Javascript projects. It has
support for accessing Sqlite, Postgres, MySQL and MariaDB databases.
The aim is to provide a *Separation of Concerns* between your application
code and your database code so that you can write application code at a
higher level of abstraction, with the details of the database hidden away
in the lower levels.
## Philosophy
It is based on the philosophy that ORMs and SQL query builders are considered
*Mostly Harmful*. SQL is an industry standard and has been for nearly 40
years. Although there are some minor differences between dialects, it is
the most portable and widely understood way to communicate with a relational
database. Any developer who has experience with using relational databases
should know at least the basics of SQL, regardless of the programming language
or database toolkits they are most familiar with.
Unlike most ORMs and SQL query builders which try to insulate developers from
SQL, this library embraces it and encourages you to use it in the way it was
intended to be used. One of the keys benefits is transparency. Your SQL
queries should not be hidden behind an abstraction that can obscure the
intention or subtly transform the meaning. This avoids a whole class of
"translation errors" that can result in the generated queries returning
the wrong results, being inefficient, or hard to reason about.
That said, there are a number of useful benefits that ORMs and SQL query
builders provide which this library has adopted.
* Abstraction of the underlying database engine. Although it's probably not
that common for a project to migrate from one database engine to another
(and if that does happens you'll have plenty of other things to worry about),
it is quite common for developers to work on a number of projects over a
period of time that use different databases. Having a library that
smooths over the differences between them can make it easier to switch from
one project to another.
* Automatic generation of "trivial" queries to insert, select, update and delete
records (aka "CRUD" - create, read, update, delete). As well as removing the
need to write lots of "boilerplate" queries to get your project up and running,
this is also useful when you modify tables at a later date to add or remove
columns. Those basic operations should automatically adapt to the new
schema without requiring you to rewrite lots of queries.
* The ability to compose complex queries in parts, allowing SQL fragments
to be reused in different queries that are similar, but not identical.
Doing this programmatically can not only save time, but also avoid potential
errors, either when writing them initially, or when updating them at a later
date to accommodate changes in the database schema.
* Entity models to help organise table and record-based code. Each database
table can have its own table module defined where you can add custom methods
for inserting, selecting or performing other operations on rows in the table.
Similarly, every entity type can have its own record module where you can
add methods for performing operations on an individual entity instance. This
is a lightweight variant of the Active Record pattern.
## Documentation and Examples
The [web site](https://abw.github.io/badger-database-js/)
has detailed documentation and examples of use.
The [github repository](https://github.com/abw/badger-database-js) includes a
number of [examples](https://github.com/abw/badger-database-js/tree/master/examples/)
taken from the manual that demonstrate the functionality.
## Stability
Version 1.x.x should be considered an alpha release which is subject
to change.
Version 2.0.0 will be the first stable release.
## Caveat
This is currently a work in progress loosely based on the Perl
[Badger::Database](https://github.com/abw/Badger-Database) library.
It is being written to help migrate a number of old Perl projects
to Javascript.
Feel free to use it for your own projects but be warned that
I wrote it to help me get my own job done. I don't plan for it to
be the next big thing, especially if that involves endless discussion
about why *this* library is better than *that* library and having to
justify its existence. Nor do I want to spend excessive amounts of
time promoting it, supporting it, updating it, or adding features
that aren't immediately useful to me. Pull requests for bug fixes
are always welcome. Bug reports are also welcome, but if it doesn't
come with a pull request that fixes the bug then it's less likely to
make it to the top of my TODO list. Iif you're looking to add a new
feature then please [discuss it with me](mailto:abw@wardley.org) first.
All that said, it's a relatively simple project totalling less
than 3,000 lines of code. It's got an extensive test suite of over
1,300 tests (adding another 10,000 lines of code) and a comprehensive
user manual. An experienced Javascript programmer with knowledge of SQL
should be able to grok the code in a few hours. If you're happy to use
the source, Luke, then it may be the droids you're looking for. But
if you're looking for a fully-featured, production-ready solution with
VC funding to pay for commercial-grade support and extensive promotion
then it might not be for you - there are *plenty* of other Javascript
ORMs that might be a better place to start.
If you are using it to develop an application and want to hire me for
some paid consultancy work, to provide support, or if you need someone
to help with systems analysis, database design, application development,
etc., then feel to [contact me](mailto:abw@wardley.org) to discuss your
requirements - it's what I do professionally. That isn't supposed to be
an advert, though. I'm not actively seeking work, at least, not *full-time*
work, as I've already got plenty on for a number of different clients. But
if you need me, and depending on how big a project it is, then I may be
able to fit you in or recommend someone who can.
## Installation
Use your favourite package manager (we'll assume `npm` in these examples)
to install `@abw/badger-database` and at least one of the driver modules.
```sh
$ npm install @abw/badger-database
# Then add one of the following:
$ npm install pg
$ npm install better-sqlite3
$ npm install mysql2
```
## Quick Start
Import the [connect()](https://abw.github.io/badger-database-js/connecting.html)
function from `@abw/badger-database` and create a database connection. This example shows a
`sqlite` in-memory database which is ideal for testing.
```js
import connect from '@abw/badger-database'
const db = connect({
database: 'sqlite:memory',
})
```
Use the [run()](https://abw.github.io/badger-database-js/basic-queries.html#run)
method to run SQL queries. For example, to create a `users` table:
```js
await db.run(`
CREATE TABLE users (
id INTEGER PRIMARY KEY ASC,
name TEXT,
email TEXT
)`
)
```
Or to insert a row of data:
```js
const insert = await db.run(
'INSERT INTO users (name, email) VALUES (?, ?)',
['Bobby Badger', 'bobby@badgerpower.com']
);
console.log("Inserted ID:", insert.lastInsertRowid);
```
Use the [one()](https://abw.github.io/badger-database-js/basic-queries.html#one)
method to fetch a row of data:
```js
const select = await db.one(
'SELECT name, email FROM users WHERE email=?',
['bobby@badgerpower.com']
);
console.log("User Name:", select.name);
```
Define [named queries](https://abw.github.io/badger-database-js/named-queries.html)
and reusable [query fragments](https://abw.github.io/badger-database-js/query-fragments.html)
up front so that you don't have to embed SQL in your application code:
```js
const db = connect({
database: 'sqlite:memory',
fragments: {
selectUser: `
SELECT name, email
FROM users
`
},
queries: {
createUsers: `
CREATE TABLE users (
id INTEGER PRIMARY KEY ASC,
name TEXT,
email TEXT
)`,
insertUser: `
INSERT INTO users (name, email)
VALUES (?, ?)
`,
selectUserByEmail: `
<selectUser>
WHERE email=?
`,
selectUserByName: `
<selectUser>
WHERE name=?
`,
}
})
// run named query to create table
await db.run('createUsers');
// run named query to insert a row
await db.run(
'insertUser',
['Bobby Badger', 'bobby@badgerpower.com']
);
// run named query to fetch one row
const select1 = await db.one(
'selectUserByEmail',
['bobby@badgerpower.com']
);
// another named query to fetch one row
const select2 = await db.one(
'selectUserByName',
['Bobby Badger']
);
```
Use the [query builder](https://abw.github.io/badger-database-js/query-builder.html)
to generate custom SQL select queries.
```js
const employee = db
.select(
'users.name employees.job_title',
['companies.name', 'company_name'] // alias companies.name to company_name
)
.from('users')
.join('users.id=employees.user_id')
.join('employees.company_id=companies.id')
.where('companies.id')
// Generates SQL query:
// SELECT "users"."name", "employees"."job_title", "companies"."name" AS "company_name"
// FROM "users"
// JOIN "employees" ON "users"."id" = "employees"."user_id"
// JOIN "companies" ON "employees"."company_id" = "companies"."id"
// WHERE "companies"."id" = ?
```
You can run the query multiple times:
```js
const emps1 = await employee.all([12345]) // companies.id = 12345
const emps2 = await employee.all([98765]) // companies.id = 98765
```
Queries are immutable and idempotent so you can safely extend them to
build new queries. The original query (`employee` in this example) isn't
affected in any way.
```js
const emp1 = await employee // extend existing query
.select('employees.start_date') // also select this column
.where('users.id') // add a new constraint
.one([12345, 4242]) // companies.id = 12345, users.id = 4242
// Generates SQL query:
// SELECT "users"."name", "employees"."job_title", "companies"."name" AS "company_name",
// "employees"."start_date"
// FROM "users"
// JOIN "employees" ON "users"."id" = "employees"."user_id"
// JOIN "companies" ON "employees"."company_id" = "companies"."id"
// WHERE "companies"."id" = ?
// AND "users"."id" = ?
```
Define [tables](https://abw.github.io/badger-database-js/tables.html)
to benefit from table-scoped queries and automatically generated queries:
```js
const db = connect({
database: 'sqlite:memory',
tables: {
users: {
columns: 'id:readonly name:required email:required',
queries: {
create: `
CREATE TABLE users (
id INTEGER PRIMARY KEY ASC,
name TEXT,
email TEXT
)`,
}
}
}
})
// fetch table
const users = await db.table('users');
// run named query defined for table
await users.run('create');
// insert a row
await users.insert({
name: 'Bobby Badger',
email: 'bobby@badgerpower.com',
});
// fetch one row
const select = await users.fetchOne({
email: 'bobby@badgerpower.com'
});
```
Use [records](https://abw.github.io/badger-database-js/records.html)
to perform further operations on rows.
```js
// fetch one record
const record = await users.fetchOneRecord({
email: 'bobby@badgerpower.com'
});
console.log(record.name); // Bobby Badger
console.log(record.email); // bobby@badgerpower.com
// update record
await record.update({
name: 'Robert Badger'
});
// delete record
await record.delete();
```
Define [relations](https://abw.github.io/badger-database-js/relations.html)
between tables.
```js
const db = connect({
database: 'postgres://musicdb',
tables: {
artists: {
columns: 'id name',
relations: {
// each arist has (potentially) many albums
albums: 'id => albums:artist_id'
}
},
albums: {
columns: 'id artist_id title year',
relations: {
// each album has one artist
artist: 'artist_id -> artists.id',
// each albums has many tracks
tracks: 'id => tracks.album_id',
}
},
tracks: {
columns: 'id album_id title',
relations: {
// each track appears on one album
album: 'album_id -> albums.id',
}
}
}
})
// fetch artists table
const artists = await db.table('artists');
// fetch record for Pink Floyd
const floyd = await artists.oneRecord({ name: 'Pink Floyd' });
// fetch albums by Pink Floyd
const albums = await floyd.albums;
// first album returned, e.g. Dark Side of the Moon
const album = albums[0];
console.log(album.title); // Dark Side of the Moon
// artist relation leads back to Pink Floyd
const artist = await album.artist;
console.log(artist.name); // Pink Floyd
// fetch tracks for album
const tracks = await album.tracks;
console.log(tracks[0].title); // Speak to Me
console.log(tracks[1].title); // Breathe
console.log(tracks[2].title); // On the Run
```
Read the [fine manual](https://abw.github.io/badger-database-js/) for
further information.
# Author
Andy Wardley <abw@wardley.org>