Database component
This component provides a flexible tool to deal with SQL. It's not an ORM, but a tool that let's you build SQL queries quickly via beautiful APIs.
Goals
- Force database interactions purely via Data Mappers. Adhere to the Single-Responsibility Principle.
- Make the component easy to use extend.
- Make API and usage as simple as possible.
Features
- Easy data filtering. This can be used for handling filters in data grids
- Smart pagination
- Relations
- Query builder
- Support for popular engines
- Short-cuts for very common operations
- Data mapper oriented
- Raw SQL support
- Query logging
Why is it better than raw PDO and ORMs?
When writing queries via raw PDO, you end up with lot of duplications. For instance, binding placeholders everywhere and all the time and calling prepare()
-> fetch()
can be considered as code duplication. When it comes to building dynamic queries, it would get messy very quickly.
As for why it's better than ORMs, this is because it doesn't hide the SQL, but helps you to build it, so you can take advantage of particular database engine when needed. Still, there are shortcut-methods, that look similar to ORM's common methods.
Configuration
The configuration data (typically located at /config/app.php
) is located under db
component.
There are several database drivers, that you can define:
- mysql [required options:
host
,dbname
,username
,password
] - postgresql [required options:
host
,dbname
,user
,password
] - sqlite [required options:
path
] - sqlserver [required options:
host
,dbname
,username
,password
]
The configuration must contain driver name and its associated array with options. As an example, the configuration for MySQL on a local machine would look like so:
'db' => array(
'mysql' => array(
'host' => 'localhost',
'dbname' => 'your_db_name',
'username' => 'root',
'password' => ''
)
)
Getting started
To get started, you need to create some data mapper class, that extends \Krystal\Sql\AbstractMapper
. Once you've done, you can start writing methods that abstract table access. When writing those methods, you would want to take advantage of the power of query builder component. For example, you first mapper might look like so:
<?php
namespace SomeNamespace;
use Krystal\Sql\AbstractMapper;
final class BookMapper extends AbstractMapper
{
private $table = 'some_table_name';
public function fetchById($id)
{
return $this->db->select('*')
->from($this->table)
->whereEquals('id', $id)
->query();
}
public function deleteById($id)
{
return $this->db->delete()
->from($this->table)
->whereEquals('id', $id)
->execute();
}
}
As you noted, there's an instance, which is db
that builds a query and then queries a database. Now let's take a look, what methods query builder offers out of the box for you.
Building queries
select()
\Krystal\Db\Sql\Db::select ( array|string = $type, $distinct = false)
This method builds SELECT
query fragment. It may accept an array or a string. If you supply an array, then you have two options:
$this->db->select(array('name', 'email'))
That would build a fragment like this: SELECT name, email
, wrapping them in back-ticks also.
You can use array values to specify an alias to a column, like this:
$this->db->select(array('foo' => 'bar'))
That would build a fragment which would look like as:
SELECT foo as bar
And lastly, you can pass a string, like this
$this->db->select('*')
That would build a fragment like this one:
SELECT *
If you pass a second argument as true, the it would append DISTINCT
right after SELECT
from()
\Krystal\Db\Sql\Db::from($table = null)
This method specifies a source to be selected from. It most cases that is a table name, but sometimes you might want to use several tables when building UNION
queries when building complex queries. This method is usually gets called right after select()
, like this:
$this->db->select('*')
->from('some_table')
That would build a fragment like this:
SELECT * FROM some_table
where()
\Krystal\Db\Sql\Db::where($column, $operator, $value, $filter = false)
This method simply appends WHERE
clause and usually gets called right after from()
. For instance, this call:
$this->db->select('*')
->from('some_table')
->where('id', '=', '1')
Will generate the following query:
SELECT * FROM some_table WHERE id = '1'
Lastly, there's a 4-th argument which is called $filter
. If you set it to true
, then WHERE
clause is appended only case its $value
is not empty.
There are several shortcut methods for where()
, that substitute operators.
whereLike($column, $value, $filter = false)
whereNotEquals($column, $value, $filter = false)
whereEquals($column, $value, $filter = false)
whereLessThan($column, $value, $filter = false)
whereLessThanOrEquals($column, $value, $filter = false)
whereGreaterThan($column, $value, $filter = false)
whereGreaterThanOrEquals($column, $value, $filter = false)
andWhere()
\Krystal\Db\Sql\Db::andWhere($column, $operator, $value, $filter = false)
This method appends AND WHERE
fragment. It should not be used as a first WHERE
clause, but should be used for following. Just like this:
$this->db->select('*')
->from('users')
->where('name', '=', 'John')
->andWhere('lastname', '=', 'Doe');
That would build the following query:
SELECT * FROM users WHERE name = 'Jonh' AND WHERE lastname = 'Doe'
There are also several shortcut methods, that substitute an operator accordingly:
andWhereLike($column, $value, $filter = false)
andWhereEquals($column, $value, $filter = false)
andNotWhereEquals($column, $value, $filter = false)
andWhereLessThan($column, $value, $filter = false)
andWhereGreaterThan($column, $value, $filter = false)
andWhereEqualsOrGreaterThan($column, $value, $filter = false);
andWhereEqualsOrLessThan($column, $value, $filter = false)
orWhere()
\Krystal\Db\Sql\Db::orWhere($column, $operator, $value, $filter = false)
This methods appends OR WHERE
fragment. Just like andWhere()
it should be used as a second WHERE
clause. Like this:
$this->db->select('*')
->from('users')
->where('name', '=', 'Dave')
->orWhere('name', '=', 'Jason')
This call will produce the following query:
SELECT * FROM users WHERE name = 'Dave' OR WHERE name = 'Jason'
Just like two previous methods it has shortcut methods as well. Here they are:
orWhereEquals($column, $value, $filter = false)
orWhereNotEquals($column, $value, $filter = false)
orWhereLike($column, $value, $filter = false)
orWhereGreaterThanOrEquals($column, $value, $filter = false)
orWhereLessThanOrEquals($column, $value, $filter = false)
orWhereLessThan($column, $value, $filter = false)
orWhereGreaterThan($column, $value, $filter = false)
whereIn()
\Krystal\Db\Sql\Db::whereIn($column, array $values, $filter = false)
This method appends WHERE IN
fragment. It's used like this:
$this->db->select('*')
->from('products')
->whereIn('price', array(200, 300))
This will produce the following query:
SELECT * FROM products WHERE price IN (200, 300)
whereBetween()
\Krystal\Db\Sql\Db::whereBetween($column, $a, $b, $filter = false)
This method appends WHERE column BETWEEN value1 AND value2
clause. It's used like this:
$this->db->select('*')
->from('products')
->whereBetween('price', 200, 500)
This call will produce the following query:
SELECT * FROM products WHERE price BETWEEN 200 AND 500
limit()
\Krystal\Db\Sql\Db::limit($offset, $amount = null)
This method appends LIMIT
fragment. Since LIMIT
itself can be written in two ways, either with or without offset, you can omit second argument, which is about amount of records to be returned.
An example,
$this->db->select('*')
->from('products')
->limit(10);
This call will product the following query:
SELECT * FROM products LIMIT 10
If you pass a second argument, then it would append its value after a comma. For example:
$this->db->select('*')
->from('products')
->limit(0, 10);
Will produce:
SELECT * FROM products LIMIT 0, 10
But wait!
In most cases, you don't need to use the limit()
to build pagination logic. There's a built-in method, which is called paginate()
that does all pagination tweaks and calls limit()
internally.
groupBy()
\Krystal\Db\Sql\Db::groupBy( array|string $target)
This method appends GROUP BY
statement. As its argument it may accept either a string or an array of column names. It usually gets called right after single or a bunch of WHERE
statements.
For example, this call:
$this->db->select('*')
->from('products')
->whereLessThan('price', 1000)
->groupBy('name')
Will generate the following query:
SELECT * FROM products WHERE price < 1000 GROUP BY name
orderBy()
\Krystal\Db\Sql\Db::orderBy( array|string|\Krystal\Db\Sql\RawSqlFragmentInterface $type = null )
This method appends ORDER BY
statement. As an argument it may accept an array of column names or a single column name, or an instance of Krystal\Db\Sql\RawSqlFragmentInterface
that contains raw SQL fragment.
For example, this call:
$this->db->select('*')
->from('products')
->orderBy('price')
Will generate the following query:
SELECT * FROM products ORDER BY price
You can also define sorting options for particular columns like this:
$this->db->select('*')
->from('products')
->orderBy(array('price' => 'DESC', 'date' => 'DESC'))
This will generate the following query:
SELECT * FROM products ORDER BY price DESC, date DESC
desc()
\Krystal\Db\Sql\Db::desc()
This method simply appends DESC
. Typically it's used right after orderBy()
.
having()
\Krystal\Db\Sql\Db::having($function, $column, $operator, $value)
This methods generates HAVING function(column) operator value
fragment. Usually it gets appended at the end of a query.
For example, a call that has having()
at the end:
$this->db->
...
->having('count', 'id', '>', '100')
Will generate the following fragment:
... HAVING count(id) > 100
union()
\Krystal\Db\Sql\Db::union()
This method appends UNION
. There's also one similar method unionAll()
, that appends UNION ALL
. Here's an example of its usage:
$this->db->select('*')
->from('products')
->union()
->select('*')
->from('orders')
This will generate the following query:
SELECT * FROM products UNION SELECT * FROM orders
asAlias()
\Krystal\Db\Sql\Db::asAlias()
Since as
is a reserved word in PHP, this method is called asAlias()
. It simply appends AS
keyword to the existing stack. It can be used when building complex queries where aliases are required.
delete()
\Krystal\Db\Sql\Db::delete()
Appends DELETE
. Typically used when building a query to perform removal of some rows. An example:
$this->db->delete()
->from('users')
->whereEquals('id', '1')
This call will generate the following query:
DELETE FROM users WHERE id = 1
insert()
\Krystal\Db\Sql\Db::insert($table, array $data, $ignore = false)
Appends INSERT INTO table (...) VALUES (...)
. An example:
$data = array(
'name' => 'Dave',
'age' => '23'
);
$this->db->insert('users', $data)
This will call will generate the following query:
INSERT INTO users (name, age) VALUES ('Dave', '23')
There's a third argument $ignore
, which tells the method if IGNORE
keyword before the INTO
should be appended or not. By default, its false
.
update()
\Krystal\Db\Sql\Db::update($table, array $data)
Appends UPDATE table SET column = value....
. In most cases, right after the call, you would usually want to append whereEquals()
An example:
$data = array(
'name' => 'Dave',
'age' => '24'
);
$this->db->update('users', $data)
->whereEquals('id', '1')
This call will generate the following query:
UPDATE users SET name = 'Dave', age = '24' WHERE id = '1'
increment() & decrement()
\Krystal\Db\Sql\Db::increment($table, $column, $step = 1)
\Krystal\Db\Sql\Db::decrement($table, $column, $step = 1)
This is just wrappers around update()
that do generate increment and decrement queries for columns. An example:
$this->db->increment('products', 'views')
->whereEquals('id', '1')
This will generate the following query:
UPDATE products SET views = views + 1 WHERE id = '1'
The same signature applies to decrement()
, but it does -
instead of +
, as you might already guessed.
JOINs
There are 4 methods that generate join queries. Usually the get called right after from()
method. Here they are:
innerJoin($table, $a, $b)
leftJoin($table, $a, $b)
rightJoin($table, $a, $b)
fullJoin($table, $a, $b)
They all do append a fragment, which is%TYPE% JOIN second_table ON first_table.column = second_table.column
, where %TYPE%
is just a type of join call.
For example, this call:
$this->db->select('*')
->from('products')
->innerJoin('orders', 'products.order_id', 'orders.id')
Will generate the following query:
SELECT * FROM products
INNER JOIN orders
ON products.order_id = orders.id;
Low-level comparison methods
Sometimes, when writing complex SQL queries, you might encounter a situation that available methods can't help you to write a desired query. Here come so-called low-level comparison methods. Let's take a look at available ones:
openBracket()
closeBracket()
They open and close brackets respectively.
rawAnd()
rawOr()
They append AND
and OR
respectively. And the reason they were named with raw
prefix is because, and
and or
are reserved names in PHP and cannot be used to name methods.
compare($column, $operator, $value, $filter = false)
equals($column, $value, $filter = false)
notEquals($column, $value, $filter = false)
like($column, $value, $filter = false)
greaterThan($column, $value, $filter = false)
lessThan($column, $value, $filter = false)
greaterThanOrEquals($column, $value, $filter = false)
lessThanOrEquals($column, $value, $filter = false)
And finally these methods are similar to previous ones, except that they don't add WHERE
to expressions.
Relations
In relation databases, dealing with relation tables is a very common task. For example, if you have a blog, then its posts you'd store in one table and its comments that linked to a blog's post in another.
Of course, you can do this all yourself manually, writing JOIN
queries or you can use built-in tools, that help you to achieve the same with a one line of code.
As you might already know from the theory, there are 3 common relation types: One-To-One, One-To-Many and Many-To-Many.
So let's take a peek at available methods now.
One-To-One
\Krystal\Db\Sql\Db::asOneToOne($column, $alias, $table, $link)
Parameters
$column
Column name which is linked to another table
$alias
Virtual column name to be created for holding results from another table
$table
Slave (second) table name
$link
Linking column name from second table
Example
This is when you you have one master table where you store all common data about an entity linking one of its columns to a slave table.
Consider this:
Suppose there's a table called books
, that looks so:
id | title | year | author_id
1 | Learn PHP | 2013 | 1
2 | Learn Angular.js | 2014 | 2
And there's a table for authors, which is book_authors
:
id | name |
1 | Daniel |
2 | Mark |
To fetch a result-set in one call, you'd do something like this:
$this->db->select('*')
->from('books')
->asOneToOne('author_id', 'author', 'book_authors', 'id')
->queryAll();
One-To-Many
\Krystal\Db\Sql\Db::asOneToMany($table, $pk, $alias)
Parameters
$table
Slave (second) table name
$pk
Primary column name in second table
$alias
Virtual column name to be appended for holding a result-set.
Example
You would want to use it, when some entity might contain many attached entities. As a typical example, consider a Blog-application. Each blog entity might contain unlimited number of comments. And that could look like this:
Master table (let's call it blog_posts
) for post entities might look like this:
id | title
1 | Comparing different CMS
2 | Introduction to Angular.js
And the second (slave) table for comments (let's call it blog_comments
) might look like this:
blog_post_id | comment
1 | Nice comparison! Thanks for sharing!
2 | Now I'm getting better with Angular! Thanks!
Now let's query these tables using asOneToMany()
method:
$this->db->select('*')
->from('blog_posts')
->asOneToMany('blog_comments', 'blog_post_id', 'comments')
Many-To-Many
\Krystal\Db\Sql\DB::asManyToMany($alias, $junction, $column, $table, $pk)
Parameters
$alias
Virtual column name to be created
$junction
Junction table name. This table holds only relations between Primary Keys.
$column
Column name from junction table to be selected.
$table
Slave (second) table name.
$pk
Primary column name in slave table.
Example
Consider a typical scenario : an actor can take part in several movies, and a movie might have several actors.
Let's create 3 tables for this scenario:
Table for movies (let's call it movies
)
id | title
1 | Girls on fire
2 | How to get in America
Table for actors (let's call it actors
)
id | name
1 | Jonhy X.Y
2 | Mike T.D
3 | Karla J.J
And there's a 3-rd table (a.k.a junction) which is responsible for holding relations between movies and actors - junction
.
movie_id | actor_id
2 | 1
2 | 2
1 | 3
1 | 1
Alright, now let's select all actors with their associate movies:
$this->db->select('*')
->from('movies')
->asManyToMany('actors', 'junction', 'movie_id', 'actors', 'id')
->queryAll();
Similarly, you can fetch all movies with associated actors substituting function arguments respectively.
SQL functions
There are also several methods, that generate functions. Let's take a peek at them. Here they are:
\Krystal\Db\Sql\Db::max($column, $alias = null)
\Krystal\Db\Sql\Db::min($column, $alias = null)
\Krystal\Db\Sql\Db::avg($column, $alias = null)
\Krystal\Db\Sql\Db::sum($column, $alias = null)
\Krystal\Db\Sql\Db::count($column, $alias = null)
\Krystal\Db\Sql\Db::len($column, $alias = null)
\Krystal\Db\Sql\Db::round($column, $decimals)
\Krystal\Db\Sql\Db::rand()
\Krystal\Db\Sql\Db::now()
As you might already guessed, they all do append $function($column)
fragment. in case its second argument$alias
isn't null
, then it uses its name as an alias and generates $function($column) AS $alias
instead.
Let's take one of them, and see how its typically used:
$this->db->select()
->max('price', 'max_price')
->from('products')
This will generate the following query:
SELECT MAX(price) AS max_price FROM products
Executing queries
So far, you have learned only how to build queries. Once finish building a query, it won't be executed right away.
There are there methods, that let you execute your queries, that you built to fetch a result-set:
\Krystal\Db\Sql\Db::query($column = null, $mode = null)
\Krystal\Db\Sql\Db::queryAll($column = null, $mode = null)
\Krystal\Db\Sql\Db::queryScalar($mode = null)
The query()
fetches a single row, while queryAll()
fetches all rows. The optional argument $column
these methods have is about filtering a result-set (i.e the plain array returned array) by a column name. They also allow you to use a different PDO's fetching mode, if you specify a second argument.
The queryScalar()
is used to fetch a value of a first column.
And there's also one method to execute queries, that never return a result-set. Those queries are typically INSERT
, UPDATE
and DELETE
.
\Krystal\Db\Sql\Db::execute($rowCount = false)
Let's take a look at examples:
// Ex.1: Finding user's data by his associated id
$this->db->select('*')
->from('users')
->whereEquals('id', '1')
->query(); // <- Execute the query and get a result-set
// Ex.2: Removing user's row by associated id
$this->db->delete()
->from('users')
->whereEquals('id', '1')
->execute(); // -> Execute the query without expecting a result-set in return
// Ex.3: Removing all non-activated users
$this->db->delete()
->from('users')
->whereEquals('activated', '0')
->execute(true); // -> Execute the query returning a number of affected rows
The method execute()
might accept an optional boolean parameter $rowCount
that can be used to return a number of affected rows instead of returning boolean value.
Pagination
\Krystal\Db\Sql\Db::paginate($page, $itemsPerPage)
Dropping a large result-set into small ones by pages is a very common task every developer faces. Krystal's DB component has a smart solution to this problem, which is simple paginate()
method.
It should be always called before queryAll()
. So, let's see how it works in action:
$page = 1; // Current page
$itemsPerPage = 5; // Per page count
$books = $this->db->select('*')
->from('books')
->paginate($page, $itemsPerPage)
->queryAll();
And that's it!
Now you can simply call getPaginator()
on a mapper, since paginate()
method internally tweaks the paginator. After all you you'd pass paginator's instance to a view from a service later.
Transactions
There are four methods to handle transaction. In case you use MySQL, remember that transactions aren't supported by MyISAM engine. If you plain to use them, make sure your tables are managed by InnoDB engine.
beginTransaction()
\Krystal\Db\Sql\Db\beginTransaction()
Initiates a new transaction. Returns true on success, false on failure
inTransaction()
\Krystal\Db\Sql\Db\inTransaction()
Checks if inside a transaction. Returns true if inside, false if not
commit()
\Krystal\Db\Sql\Db\commit()
Commits a transaction. Returns true on success, false on failure.
rollBack()
\Krystal\Db\Sql\Db\rollBack()
Rolls back a transaction. If no transaction is active, then it would throw \PDOException
. Returns true on success, false on failure.
Shortcuts
A shortcut is a method that does some very common thing. Take as example these things:
-- You will be often finding a row by its associated id -- You will be often removing a row by its associated id -- You will be often inserting or removing records
And things like that.
To reduce the amount similar queries, Krystal's database component has shortcut methods.
But in order to start using them, you have to implement two methods in your mapper:
string public static function getTableName()
It must return a name of a table you're going to deal with.
and
string protected function getPk()
It must return a name of primary key in your table. Often it's called id
Alright, now let's explore each shortcut method.
findByPk()
\Krystal\Db\Sql\AbstractMapper::findByPk($id)
Finds a row by its associated id, selecting all columns and returning an array. It's an equivalent to:
$this->db->select('*')
->from(self::getTableName())
->whereEquals($this->getPk(), $id)
->query()
findColumnByPk()
\Krystal\Db\Sql\AbstractMapper::findColumnByPk($id, $column)
Finds column's value by provided PK's value. It's useful if you want to select only one column by associated id.
It's an equivalent to:
$this->db->select($column)
->from(self::getTableName())
->whereEquals($this->getPk(), $id)
->query($column);
getLastPk()
\Krystal\Db\Sql\AbstractMapper::getLastPk()
Returns a maximal value of PK. It's an equivalent to:
$this->db->select()
->max($this->getPk(), 'last')
->from(self::getTableName())
->query('last')
persist()
\Krystal\Db\Sql\AbstractMapper::persist(array $data)
This method updates an existing record or inserts a new one. If it can find a PK's name in $data
's key which is not empty, it will update a record. Otherwise it will insert a new one.
updateColumnByPk()
\Krystal\Db\Sql\AbstractMapper::updateColumnByPk()($pk, $column, $value)
Updates a single column by associated id in a table. This is an equivalent to this call:
$this->db->update(self::getTableName(), array($column => $value))
->whereEquals($this->getPk(), $pk)
->execute();
incrementColumnByPk()
\Krystal\Db\Sql\AbstractMapper::incrementColumnByPk($pk, $column, $step = 1)
Increments a value of a numeric column. This is an equivalent to:
$this->db->increment(self::getTableName(), $column, $step)
->whereEquals($this->getPk(), $pk)
->execute();
decrementColumnByPk()
\Krystal\Db\Sql\AbstractMapper::decrementColumnByPk($pk, $column, $step = 1)
Decrements a value of a numeric column. This is an equivalent to:
$this->db->decrement(self::getTableName(), $column, $step)
->whereEquals($this->getPk(), $pk)
->execute();
Debugging
Sometimes you might want to determine what queries were executed or how a current query looks like.
Current query
To view current query, you can simply do echo $this->db
, since the database object implements __toString()
method.
Query logger
Query logger can be accessed like, $this->db->getQueryLogger()
and it has two available methods.
getAll()
getCount()
getAll()
returns all executed queries, and getCount()
returns an amount of executed queries.
Raw queries
It's not a secret that it's very hard to abstract all possible SQL queries, especially for various database engines. Like most popular tools for SQL out there, Krystal abstracts only very common queries. In case you need to execute a non-trivial query, you can do so by invoking raw()
method on db
service object.
For example:
$db = $this->db->raw('SELECT .... UNION SELECT ... WHERE `name` = :name', array(
// Here come bindings
':name' => 'Jonh Doe'
));
$data = $db->queryAll(); // For all matching rows
$data = $db->query(); // For only first matching row
In case you don't expect a result-set back, then you'd better use $db->execute()
.
Using raw PDO
Sometimes when using 3-rd party libraries, you might encounter a situation where they require an instance of PDO. To access a raw PDO instance, just call getPdo()
on database object, like this:
$pdo = $this->db->getPdo();