Commit d67a4e7b authored by Benjamin Eberlei's avatar Benjamin Eberlei

Add 'docs/' from commit 'b605286d'

git-subtree-dir: docs
git-subtree-mainline: 30b5c31a
git-subtree-split: b605286d
parents 30b5c31a b605286d
en/_exts/configurationblock.pyc
build
en/_build
[submodule "en/_theme"]
path = en/_theme
url = https://github.com/doctrine/doctrine-sphinx-theme.git
# Doctrine DBAL Documentation
## How to Generate
1. Run ./bin/install-dependencies.sh
2. Run ./bin/generate-docs.sh
It will generate the documentation into the build directory of the checkout.
\ No newline at end of file
#!/bin/bash
EXECPATH=`dirname $0`
cd $EXECPATH
cd ..
rm build -Rf
sphinx-build en build
sphinx-build -b latex en build/pdf
rubber --into build/pdf --pdf build/pdf/DoctrineDBAL.tex
\ No newline at end of file
#!/bin/bash
sudo apt-get install python25 python25-dev texlive-full rubber
sudo easy_install pygments
sudo easy_install sphinx
\ No newline at end of file
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DoctrineDBAL.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DoctrineDBAL.qhc"
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
"run these through (pdf)latex."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
#Copyright (c) 2010 Fabien Potencier
#
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is furnished
#to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all
#copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#THE SOFTWARE.
from docutils.parsers.rst import Directive, directives
from docutils import nodes
from string import upper
class configurationblock(nodes.General, nodes.Element):
pass
class ConfigurationBlock(Directive):
has_content = True
required_arguments = 0
optional_arguments = 0
final_argument_whitespace = True
option_spec = {}
formats = {
'html': 'HTML',
'xml': 'XML',
'php': 'PHP',
'yaml': 'YAML',
'jinja': 'Twig',
'html+jinja': 'Twig',
'jinja+html': 'Twig',
'php+html': 'PHP',
'html+php': 'PHP',
'ini': 'INI',
'php-annotations': 'Annotations',
}
def run(self):
env = self.state.document.settings.env
node = nodes.Element()
node.document = self.state.document
self.state.nested_parse(self.content, self.content_offset, node)
entries = []
for i, child in enumerate(node):
if isinstance(child, nodes.literal_block):
# add a title (the language name) before each block
#targetid = "configuration-block-%d" % env.new_serialno('configuration-block')
#targetnode = nodes.target('', '', ids=[targetid])
#targetnode.append(child)
innernode = nodes.emphasis(self.formats[child['language']], self.formats[child['language']])
para = nodes.paragraph()
para += [innernode, child]
entry = nodes.list_item('')
entry.append(para)
entries.append(entry)
resultnode = configurationblock()
resultnode.append(nodes.bullet_list('', *entries))
return [resultnode]
def visit_configurationblock_html(self, node):
self.body.append(self.starttag(node, 'div', CLASS='configuration-block'))
def depart_configurationblock_html(self, node):
self.body.append('</div>\n')
def visit_configurationblock_latex(self, node):
pass
def depart_configurationblock_latex(self, node):
pass
def setup(app):
app.add_node(configurationblock,
html=(visit_configurationblock_html, depart_configurationblock_html),
latex=(visit_configurationblock_latex, depart_configurationblock_latex))
app.add_directive('configuration-block', ConfigurationBlock)
_theme @ 5aa024c9
Subproject commit 5aa024c9284228ed76c7a9aa785229cf19f396e3
# -*- coding: utf-8 -*-
#
# Doctrine DBAL documentation build configuration file, created by
# sphinx-quickstart on Mon Nov 1 19:50:57 2010.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.append(os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Doctrine DBAL'
copyright = u'2010, Roman Borschel, Guilherme Blanco, Benjamin Eberlei, Jonathan Wage'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '2.1'
# The full version, including alpha/beta/rc tags.
release = '2.1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
language = 'en'
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of documents that shouldn't be included in the build.
#unused_docs = []
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
exclude_trees = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
highlight_language = 'php'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'doctrine'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = ['_theme']
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
#html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_use_modindex = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = ''
# Output file base name for HTML help builder.
htmlhelp_basename = 'DoctrineDBALdoc'
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'DoctrineDBAL.tex', u'Doctrine DBAL Documentation',
u'Roman Borschel, Guilherme Blanco, Benjamin Eberlei, Jonathan Wage', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_use_modindex = True
.. Doctrine DBAL documentation master file, created by
sphinx-quickstart on Mon Nov 1 19:16:59 2010.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to Doctrine DBAL's documentation!
=========================================
Contents:
.. toctree::
:maxdepth: 1
:numbered:
reference/introduction
reference/architecture
reference/configuration
reference/data-retrieval-and-manipulation
reference/query-builder
reference/transactions
reference/platforms
reference/types
reference/schema-manager
reference/schema-representation
reference/events
reference/security
reference/sharding
reference/sharding_azure_tutorial
reference/supporting-other-databases
reference/portability
reference/caching
reference/known-vendor-issues
Indices and tables
==================
* :ref:`search`
@ECHO OFF
REM Command file for Sphinx documentation
set SPHINXBUILD=sphinx-build
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\DoctrineDBAL.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\DoctrineDBAL.ghc
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
:end
Architecture
============
As already said, the DBAL is a thin layer on top of PDO. PDO itself
is mainly defined in terms of 2 classes: ``PDO`` and
``PDOStatement``. The equivalent classes in the DBAL are
``Doctrine\DBAL\Connection`` and ``Doctrine\DBAL\Statement``. A
``Doctrine\DBAL\Connection`` wraps a
``Doctrine\DBAL\Driver\Connection`` and a
``Doctrine\DBAL\Statement`` wraps a
``Doctrine\DBAL\Driver\Statement``.
``Doctrine\DBAL\Driver\Connection`` and
``Doctrine\DBAL\Driver\Statement`` are just interfaces. These
interfaces are implemented by concrete drivers. For all PDO based
drivers, ``PDO`` and ``PDOStatement`` are the implementations of
these interfaces. Thus, for PDO-based drivers, a
``Doctrine\DBAL\Connection`` wraps a ``PDO`` instance and a
``Doctrine\DBAL\Statement`` wraps a ``PDOStatement`` instance. Even
more, a ``Doctrine\DBAL\Connection`` *is a*
``Doctrine\DBAL\Driver\Connection`` and a
``Doctrine\DBAL\Statement`` *is a*
``Doctrine\DBAL\Driver\Statement``.
What does a ``Doctrine\DBAL\Connection`` or a
``Doctrine\DBAL\Statement`` add to the underlying driver
implementations? The enhancements include SQL logging, events and
control over the transaction isolation level in a portable manner,
among others.
A DBAL driver is defined to the outside in terms of 3 interfaces:
``Doctrine\DBAL\Driver``, ``Doctrine\DBAL\Driver\Connection`` and
``Doctrine\DBAL\Driver\Statement``. The latter two resemble (a
subset of) the corresponding PDO API.
A concrete driver implementation must provide implementation
classes for these 3 interfaces.
The DBAL is separated into several different packages that
perfectly separate responsibilities of the different RDBMS layers.
Drivers
-------
The drivers abstract a PHP specific database API by enforcing two
interfaces:
- ``\Doctrine\DBAL\Driver\Driver``
- ``\Doctrine\DBAL\Driver\Statement``
The above two interfaces require exactly the same methods as PDO.
Platforms
---------
The platforms abstract the generation of queries and which database
features a platform supports. The
``\Doctrine\DBAL\Platforms\AbstractPlatform`` defines the common
denominator of what a database platform has to publish to the
userland, to be fully supportable by Doctrine. This includes the
SchemaTool, Transaction Isolation and many other features. The
Database platform for MySQL for example can be used by all 3 MySQL
extensions, PDO, Mysqli and ext/mysql.
Logging
-------
The logging holds the interface and some implementations for
debugging of Doctrine SQL query execution during a request.
Schema
------
The schema offers an API for each database platform to execute DDL
statements against your platform or retrieve metadata about it. It
also holds the Schema Abstraction Layer which is used by the
different Schema Management facilities of Doctrine DBAL and ORM.
Types
-----
The types offer an abstraction layer for the converting and
generation of types between Databases and PHP. Doctrine comes
bundled with some common types but offers the ability for
developers to define custom types or extend existing ones easily.
Caching
=======
A ``Doctrine\DBAL\Statement`` can automatically cache result sets.
For this to work an instance of ``Doctrine\Common\Cache\Cache`` must be provided.
This can be set on the configuration object (optionally it can also be passed at query time):
::
<?php
$cache = new \Doctrine\Common\Cache\ArrayCache();
$config = $conn->getConfiguration();
$config->setResultCacheImpl($cache);
To get the result set of a query cached it is necessary to pass a
``Doctrine\DBAL\Cache\QueryCacheProfile`` instance to the ``executeQuery`` or ``executeCacheQuery``
instance. The difference between these two methods is that the former does not
require this instance, while the later has this instance as a required parameter:
::
<?php
$stmt = $conn->executeQuery($query, $params, $types, new QueryCacheProfile(0, "some key"));
$stmt = $conn->executeCachedQuery($query, $params, $types, new QueryCacheProfile(0, "some key"));
It is also possible to pass in a the ``Doctrine\Common\Cache\Cache`` instance into the
constructor of ``Doctrine\DBAL\Cache\QueryCacheProfile`` in which case it overrides
the default cache instance:
::
<?php
$cache = new \Doctrine\Common\Cache\FilesystemCache(__DIR__);
new QueryCacheProfile(0, "some key", $cache);
In order for the data to actually be cached its necessary to ensure that the entire
result set is read (easiest way to ensure this is to use ``fetchAll``) and the statement
object is closed:
::
<?php
$stmt = $conn->executeCachedQuery($query, $params, $types, new QueryCacheProfile(0, "some key"));
$data = $stmt->fetchAll();
$stmt->close() // at this point the result is cached
.. warning::
When using the cache layer not all fetch modes are supported. See the code of the `ResultCacheStatement <https://github.com/doctrine/dbal/blob/master/lib/Doctrine/DBAL/Cache/ResultCacheStatement.php#L156>`_ for details.
\ No newline at end of file
Configuration
=============
Getting a Connection
--------------------
You can get a DBAL Connection through the
``Doctrine\DBAL\DriverManager`` class.
.. code-block:: php
<?php
$config = new \Doctrine\DBAL\Configuration();
//..
$connectionParams = array(
'dbname' => 'mydb',
'user' => 'user',
'password' => 'secret',
'host' => 'localhost',
'driver' => 'pdo_mysql',
);
$conn = \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $config);
The ``DriverManager`` returns an instance of
``Doctrine\DBAL\Connection`` which is a wrapper around the
underlying driver connection (which is often a PDO instance).
The following sections describe the available connection parameters
in detail.
Driver
~~~~~~
The driver specifies the actual implementations of the DBAL
interfaces to use. It can be configured in one of three ways:
- ``driver``: The built-in driver implementation to use. The
following drivers are currently available:
- ``pdo_mysql``: A MySQL driver that uses the pdo\_mysql PDO
extension.
- ``pdo_sqlite``: An SQLite driver that uses the pdo\_sqlite PDO
extension.
- ``pdo_pgsql``: A PostgreSQL driver that uses the pdo\_pgsql PDO
extension.
- ``pdo_oci``: An Oracle driver that uses the pdo\_oci PDO
extension.
**Note that this driver caused problems in our tests. Prefer the oci8 driver if possible.**
- ``pdo_sqlsrv``: An MSSQL driver that uses pdo\_sqlsrv PDO
- ``oci8``: An Oracle driver that uses the oci8 PHP extension.
- ``driverClass``: Specifies a custom driver implementation if no
'driver' is specified. This allows the use of custom drivers that
are not part of the Doctrine DBAL itself.
- ``pdo``: Specifies an existing PDO instance to use.
Wrapper Class
~~~~~~~~~~~~~
By default a ``Doctrine\DBAL\Connection`` is wrapped around a
driver ``Connection``. The ``wrapperClass`` option allows to
specify a custom wrapper implementation to use, however, a custom
wrapper class must be a subclass of ``Doctrine\DBAL\Connection``.
Connection Details
~~~~~~~~~~~~~~~~~~
The connection details identify the database to connect to as well
as the credentials to use. The connection details can differ
depending on the used driver. The following sections describe the
options recognized by each built-in driver.
.. note::
When using an existing PDO instance through the ``pdo``
option, specifying connection details is obviously not necessary.
pdo\_sqlite
^^^^^^^^^^^
- ``user`` (string): Username to use when connecting to the
database.
- ``password`` (string): Password to use when connecting to the
database.
- ``path`` (string): The filesystem path to the database file.
Mutually exclusive with ``memory``. ``path`` takes precedence.
- ``memory`` (boolean): True if the SQLite database should be
in-memory (non-persistent). Mutually exclusive with ``path``.
``path`` takes precedence.
pdo\_mysql
^^^^^^^^^^
- ``user`` (string): Username to use when connecting to the
database.
- ``password`` (string): Password to use when connecting to the
database.
- ``host`` (string): Hostname of the database to connect to.
- ``port`` (integer): Port of the database to connect to.
- ``dbname`` (string): Name of the database/schema to connect to.
- ``unix_socket`` (string): Name of the socket used to connect to
the database.
- ``charset`` (string): The charset used when connecting to the
database.
pdo\_pgsql
^^^^^^^^^^
- ``user`` (string): Username to use when connecting to the
database.
- ``password`` (string): Password to use when connecting to the
database.
- ``host`` (string): Hostname of the database to connect to.
- ``port`` (integer): Port of the database to connect to.
- ``dbname`` (string): Name of the database/schema to connect to.
pdo\_oci / oci8
^^^^^^^^^^^^^^^
- ``user`` (string): Username to use when connecting to the
database.
- ``password`` (string): Password to use when connecting to the
database.
- ``host`` (string): Hostname of the database to connect to.
- ``port`` (integer): Port of the database to connect to.
- ``dbname`` (string): Name of the database/schema to connect to.
- ``charset`` (string): The charset used when connecting to the
database.
pdo\_sqlsrv
^^^^^^^^^^
- ``user`` (string): Username to use when connecting to the
database.
- ``password`` (string): Password to use when connecting to the
database.
- ``host`` (string): Hostname of the database to connect to.
- ``port`` (integer): Port of the database to connect to.
- ``dbname`` (string): Name of the database/schema to connect to.
Custom Platform
~~~~~~~~~~~~~~~
Each built-in driver uses a default implementation of
``Doctrine\DBAL\Platforms\AbstractPlatform``. If you wish to use a
customized or custom implementation, you can pass a precreated
instance in the ``platform`` option.
Custom Driver Options
~~~~~~~~~~~~~~~~~~~~~
The ``driverOptions`` option allows to pass arbitrary options
through to the driver. This is equivalent to the fourth argument of
the `PDO constructor <http://php.net/manual/en/pdo.construct.php>`_.
Data Retrieval And Manipulation
===============================
Doctrine DBAL follows the PDO API very closely. If you have worked with PDO
before you will get to know Doctrine DBAL very quickly. On top of the API provided
by PDO there are tons of convenience functions in Doctrine DBAL.
Data Retrieval
--------------
Using a database implies retrieval of data. It is the primary use-case of a database.
For this purpose each database vendor exposes a Client API that can be integrated into
programming languages. PHP has a generic abstraction layer for this
kind of API called PDO (PHP Data Objects). However because of disagreements
between the PHP community there are often native extensions for each database
vendor that are much more maintained (OCI8 for example).
Doctrine DBAL API builds on top of PDO and integrates native extensions by
wrapping them into the PDO API as well. If you already have an open connection
through the ``Doctrine\DBAL\DriverManager::getConnection()`` method you
can start using this API for data retrieval easily.
Start writing an SQL query and pass it to the ``query()`` method of your
connection:
.. code-block:: php
<?php
use Doctrine\DBAL\DriverManager;
$conn = DriverManager::getConnection($params, $config);
$sql = "SELECT * FROM articles";
$stmt = $conn->query($sql); // Simple, but has several drawbacks
The query method executes the SQL and returns a database statement object.
A database statement object can be iterated to retrieve all the rows that matched
the query until there are no more rows:
.. code-block:: php
<?php
while ($row = $stmt->fetch()) {
echo $row['headline'];
}
The query method is the most simple one for fetching data, but it also has
several drawbacks:
- There is no way to add dynamic parameters to the SQL query without modifying
``$sql`` itself. This can easily lead to a category of security
holes called **SQL injection**, where a third party can modify the SQL executed
and even execute their own queries through clever exploiting of the security hole.
- **Quoting** dynamic parameters for an SQL query is tedious work and requires lots
of use of the ``Doctrine\DBAL\Connection#quote()`` method, which makes the
original SQL query hard to read/understand.
- Databases optimize SQL queries before they are executed. Using the query method
you will trigger the optimization process over and over again, although
it could re-use this information easily using a technique called **prepared statements**.
This three arguments and some more technical details hopefully convinced you to investigate
prepared statements for accessing your database.
Dynamic Parameters and Prepared Statements
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Consider the previous query, now parameterized to fetch only a single article by id.
Using **ext/mysql** (still the primary choice of MySQL access for many developers) you had to escape
every value passed into the query using ``mysql_real_escape_string()`` to avoid SQL injection:
.. code-block:: php
<?php
$sql = "SELECT * FROM articles WHERE id = '" . mysql_real_escape_string($id, $link) . "'";
$rs = mysql_query($sql);
If you start adding more and more parameters to a query (for example in UPDATE or INSERT statements)
this approach might lead to complex to maintain SQL queries. The reason is simple, the actual
SQL query is not clearly separated from the input parameters. Prepared statements separate
these two concepts by requiring the developer to add **placeholders** to the SQL query (prepare) which
are then replaced by their actual values in a second step (execute).
.. code-block:: php
<?php
// $conn instanceof Doctrine\DBAL\Connection
$sql = "SELECT * FROM articles WHERE id = ?";
$stmt = $conn->prepare($sql);
$stmt->bindValue(1, $id);
$stmt->execute();
Placeholders in prepared statements are either simple positional question marks (?) or named labels starting with
a double-colon (:name1). You cannot mix the positional and the named approach. The approach
using question marks is called positional, because the values are bound in order from left to right
to any question mark found in the previously prepared SQL query. That is why you specify the
position of the variable to bind into the ``bindValue()`` method:
.. code-block:: php
<?php
// $conn instanceof Doctrine\DBAL\Connection
$sql = "SELECT * FROM articles WHERE id = ? AND status = ?";
$stmt = $conn->prepare($sql);
$stmt->bindValue(1, $id);
$stmt->bindValue(2, $status);
$stmt->execute();
Named parameters have the advantage that their labels can be re-used and only need to be bound once:
.. code-block:: php
<?php
// $conn instanceof Doctrine\DBAL\Connection
$sql = "SELECT * FROM users WHERE name = :name OR username = :name";
$stmt = $conn->prepare($sql);
$stmt->bindValue("name", $name);
$stmt->execute();
The following section describes the API of Doctrine DBAL with regard to prepared statements.
.. note::
Support for positional and named prepared statements varies between the different
database extensions. PDO implements its own client side parser so that both approaches
are feasible for all PDO drivers. OCI8/Oracle only supports named parameters, but
Doctrine implements a client side parser to allow positional parameters also.
Using Prepared Statements
~~~~~~~~~~~~~~~~~~~~~~~~~
There are three low-level methods on ``Doctrine\DBAL\Connection`` that allow you to
use prepared statements:
- ``prepare($sql)`` - Create a prepared statement of the type ``Doctrine\DBAL\Statement``.
Using this method is preferred if you want to re-use the statement to execute several
queries with the same SQL statement only with different parameters.
- ``executeQuery($sql, $params, $types)`` - Create a prepared statement for the passed
SQL query, bind the given params with their binding types and execute the query.
This method returns the executed prepared statement for iteration and is useful
for SELECT statements.
- ``executeUpdate($sql, $params, $types)`` - Create a prepared statement for the passed
SQL query, bind the given params with their binding types and execute the query.
This method returns the number of affected rows by the executed query and is useful
for UPDATE, DELETE and INSERT statements.
A simple usage of prepare was shown in the previous section, however it is useful to
dig into the features of a ``Doctrine\DBAL\Statement`` a little bit more. There are essentially
two different types of methods available on a statement. Methods for binding parameters and types
and methods to retrieve data from a statement.
- ``bindValue($pos, $value, $type)`` - Bind a given value to the positional or named parameter
in the prepared statement.
- ``bindParam($pos, &$param, $type)`` - Bind a given reference to the positional or
named parameter in the prepared statement.
If you are finished with binding parameters you have to call ``execute()`` on the statement, which
will trigger a query to the database. After the query is finished you can access the results
of this query using the fetch API of a statement:
- ``fetch($fetchStyle)`` - Retrieves the next row from the statement or false if there are none.
Moves the pointer forward one row, so that consecutive calls will always return the next row.
- ``fetchColumn($column)`` - Retrieves only one column of the next row specified by column index.
Moves the pointer forward one row, so that consecutive calls will always return the next row.
- ``fetchAll($fetchStyle)`` - Retrieves all rows from the statement.
The fetch API of a prepared statement obviously works only for ``SELECT`` queries.
If you find it tedious to write all the prepared statement code you can alternatively use
the ``Doctrine\DBAL\Connection#executeQuery()`` and ``Doctrine\DBAL\Connection#executeUpdate()``
methods. See the API section below on details how to use them.
Additionally there are lots of convenience methods for data-retrieval and manipulation
on the Connection, which are all described in the API section below.
Binding Types
-------------
Doctrine DBAL extends PDOs handling of binding types in prepared statement
considerably. Besides the well known ``\PDO::PARAM_*`` constants you
can make use of two very powerful additional features.
Doctrine\DBAL\Types Conversion
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you don't specify an integer (through a ``PDO::PARAM*`` constant) to
any of the parameter binding methods but a string, Doctrine DBAL will
ask the type abstraction layer to convert the passed value from
its PHP to a database representation. This way you can pass ``\DateTime``
instances to a prepared statement and have Doctrine convert them
to the appropriate vendors database format:
.. code-block:: php
<?php
$date = new \DateTime("2011-03-05 14:00:21");
$stmt = $conn->prepare("SELECT * FROM articles WHERE publish_date > ?");
$stmt->bindValue(1, $date, "datetime");
$stmt->execute();
If you take a look at ``Doctrine\DBAL\Types\DateTimeType`` you will see that
parts of the conversion is delegated to a method on the current database platform,
which means this code works independent of the database you are using.
.. note::
Be aware this type conversion only works with ``Statement#bindValue()``,
``Connection#executeQuery()`` and ``Connection#executeUpdate()``. It
is not supported to pass a doctrine type name to ``Statement#bindParam()``,
because this would not work with binding by reference.
List of Parameters Conversion
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. note::
This is a Doctrine 2.1 feature.
One rather annoying bit of missing functionality in SQL is the support for lists of parameters.
You cannot bind an array of values into a single prepared statement parameter. Consider
the following very common SQL statement:
.. code-block:: sql
SELECT * FROM articles WHERE id IN (?)
Since you are using an ``IN`` expression you would really like to use it in the following way
(and I guess everybody has tried to do this once in his life, before realizing it doesn't work):
.. code-block:: php
<?php
$stmt = $conn->prepare('SELECT * FROM articles WHERE id IN (?)');
// THIS WILL NOT WORK:
$stmt->bindValue(1, array(1, 2, 3, 4, 5, 6));
$stmt->execute();
Implementing a generic way to handle this kind of query is tedious work. This is why most
developers fallback to inserting the parameters directly into the query, which can open
SQL injection possibilities if not handled carefully.
Doctrine DBAL implements a very powerful parsing process that will make this kind of prepared
statement possible natively in the binding type system.
The parsing necessarily comes with a performance overhead, but only if you really use a list of parameters.
There are two special binding types that describe a list of integers or strings:
- ``\Doctrine\DBAL\Connection::PARAM_INT_ARRAY``
- ``\Doctrine\DBAL\Connection::PARAM_STR_ARRAY``
Using one of this constants as a type you can activate the SQLParser inside Doctrine that rewrites
the SQL and flattens the specified values into the set of parameters. Consider our previous example:
.. code-block:: php
<?php
$stmt = $conn->executeQuery('SELECT * FROM articles WHERE id IN (?)',
array(array(1, 2, 3, 4, 5, 6)),
array(\Doctrine\DBAL\Connection::PARAM_INT_ARRAY)
);
The SQL statement passed to ``Connection#executeQuery`` is not the one actually passed to the
database. It is internally rewritten to look like the following explicit code that could
be specified as well:
.. code-block:: php
<?php
// Same SQL WITHOUT usage of Doctrine\DBAL\Connection::PARAM_INT_ARRAY
$stmt = $conn->executeQuery('SELECT * FROM articles WHERE id IN (?, ?, ?, ?, ?, ?)',
array(1, 2, 3, 4, 5, 6),
array(\PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT)
);
This is much more complicated and is ugly to write generically.
.. note::
The parameter list support only works with ``Doctrine\DBAL\Connection::executeQuery()``
and ``Doctrine\DBAL\Connection::executeUpdate()``, NOT with the binding methods of
a prepared statement.
API
---
The DBAL contains several methods for executing queries against
your configured database for data retrieval and manipulation. Below
we'll introduce these methods and provide some examples for each of
them.
prepare()
~~~~~~~~~
Prepare a given SQL statement and return the
``\Doctrine\DBAL\Driver\Statement`` instance:
.. code-block:: php
<?php
$statement = $conn->prepare('SELECT * FROM user');
$statement->execute();
$users = $statement->fetchAll();
/*
array(
0 => array(
'username' => 'jwage',
'password' => 'changeme
)
)
*/
executeUpdate()
~~~~~~~~~~~~~~~
Executes a prepared statement with the given SQL and parameters and
returns the affected rows count:
.. code-block:: php
<?php
$count = $conn->executeUpdate('UPDATE user SET username = ? WHERE id = ?', array('jwage', 1));
echo $count; // 1
The ``$types`` variable contains the PDO or Doctrine Type constants
to perform necessary type conversions between actual input
parameters and expected database values. See the
`Types <./types#type-conversion>`_ section for more information.
executeQuery()
~~~~~~~~~~~~~~
Creates a prepared statement for the given SQL and passes the
parameters to the execute method, then returning the statement:
.. code-block:: php
<?php
$statement = $conn->executeQuery('SELECT * FROM user WHERE username = ?', array('jwage'));
$user = $statement->fetch();
/*
array(
0 => 'jwage',
1 => 'changeme
)
*/
The ``$types`` variable contains the PDO or Doctrine Type constants
to perform necessary type conversions between actual input
parameters and expected database values. See the
`Types <./types#type-conversion>`_ section for more information.
fetchAll()
~~~~~~~~~~
Execute the query and fetch all results into an array:
.. code-block:: php
<?php
$users = $conn->fetchAll('SELECT * FROM user');
/*
array(
0 => array(
'username' => 'jwage',
'password' => 'changeme
)
)
*/
fetchArray()
~~~~~~~~~~~~
Numeric index retrieval of first result row of the given query:
.. code-block:: php
<?php
$user = $conn->fetchArray('SELECT * FROM user WHERE username = ?', array('jwage'));
/*
array(
0 => 'jwage',
1 => 'changeme
)
*/
fetchColumn()
~~~~~~~~~~~~~
Retrieve only the given column of the first result row.
.. code-block:: php
<?php
$username = $conn->fetchColumn('SELECT username FROM user WHERE id = ?', array(1), 0);
echo $username; // jwage
fetchAssoc()
~~~~~~~~~~~~
Retrieve assoc row of the first result row.
.. code-block:: php
<?php
$user = $conn->fetchAssoc('SELECT * FROM user WHERE username = ?', array('jwage'));
/*
array(
'username' => 'jwage',
'password' => 'changeme
)
*/
There are also convenience methods for data manipulation queries:
delete()
~~~~~~~~~
Delete all rows of a table matching the given identifier, where
keys are column names.
.. code-block:: php
<?php
$conn->delete('user', array('id' => 1));
// DELETE FROM user WHERE id = ? (1)
insert()
~~~~~~~~~
Insert a row into the given table name using the key value pairs of
data.
.. code-block:: php
<?php
$conn->insert('user', array('username' => 'jwage'));
// INSERT INTO user (username) VALUES (?) (jwage)
update()
~~~~~~~~~
Update all rows for the matching key value identifiers with the
given data.
.. code-block:: php
<?php
$conn->update('user', array('username' => 'jwage'), array('id' => 1));
// UPDATE user (username) VALUES (?) WHERE id = ? (jwage, 1)
By default the Doctrine DBAL does no escaping. Escaping is a very
tricky business to do automatically, therefore there is none by
default. The ORM internally escapes all your values, because it has
lots of metadata available about the current context. When you use
the Doctrine DBAL as standalone, you have to take care of this
yourself. The following methods help you with it:
quote()
~~~~~~~~~
Quote a value:
.. code-block:: php
<?php
$quoted = $conn->quote('value');
$quoted = $conn->quote('1234', \PDO::PARAM_INT);
quoteIdentifier()
~~~~~~~~~~~~~~~~~
Quote an identifier according to the platform details.
.. code-block:: php
<?php
$quoted = $conn->quoteIdentifier('id');
Events
======
Both ``Doctrine\DBAL\DriverManager`` and
``Doctrine\DBAL\Connection`` accept an instance of
``Doctrine\Common\EventManager``. The EventManager has a couple of
events inside the DBAL layer that are triggered for the user to
listen to.
PostConnect Event
-----------------
``Doctrine\DBAL\Events::postConnect`` is triggered right after the
connection to the database is established. It allows to specify any
relevant connection specific options and gives access to the
``Doctrine\DBAL\Connection`` instance that is responsible for the
connection management via an instance of
``Doctrine\DBAL\Event\ConnectionEventArgs`` event arguments
instance.
Doctrine ships with one implementation for the "PostConnect" event:
- ``Doctrine\DBAL\Event\Listeners\OracleSessionInit`` allows to
specify any number of Oracle Session related enviroment variables
that are set right after the connection is established.
You can register events by subscribing them to the ``EventManager``
instance passed to the Connection factory:
.. code-block:: php
<?php
$evm = new EventManager();
$evm->addEventSubscriber(new OracleSessionInit(array(
'NLS_TIME_FORMAT' => 'HH24:MI:SS',
)));
$conn = DriverManager::getConnection($connectionParams, null, $evm);
Introduction
============
The Doctrine database abstraction & access layer (DBAL) offers a
lightweight and thin runtime layer around a PDO-like API and a lot
of additional, horizontal features like database schema
introspection and manipulation through an OO API.
The fact that the Doctrine DBAL abstracts the concrete PDO API away
through the use of interfaces that closely resemble the existing
PDO API makes it possible to implement custom drivers that may use
existing native or self-made APIs. For example, the DBAL ships with
a driver for Oracle databases that uses the oci8 extension under
the hood.
The Doctrine 2 database layer can be used independently of the
object-relational mapper. In order to use the DBAL all you need is
the ``Doctrine\Common`` and ``Doctrine\DBAL`` namespaces. Once you
have the Common and DBAL namespaces you must setup a class loader
to be able to autoload the classes:
.. code-block:: php
<?php
use Doctrine\Common\ClassLoader;
require '/path/to/doctrine/lib/Doctrine/Common/ClassLoader.php';
$classLoader = new ClassLoader('Doctrine', '/path/to/doctrine');
$classLoader->register();
Now you are able to load classes that are in the
``/path/to/doctrine`` directory like
``/path/to/doctrine/Doctrine/DBAL/DriverManager.php`` which we will
use later in this documentation to configure our first Doctrine
DBAL connection.
Known Vendor Issues
===================
This section describes known compatability issues with all the
supported database vendors:
PostgreSQL
----------
DateTime, DateTimeTz and Time Types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Postgres has a variable return format for the datatype TIMESTAMP(n)
and TIME(n) if microseconds are allowed (n > 0). Whenever you save
a value with microseconds = 0. PostgreSQL will return this value in
the format:
::
2010-10-10 10:10:10 (Y-m-d H:i:s)
However if you save a value with microseconds it will return the
full representation:
::
2010-10-10 10:10:10.123456 (Y-m-d H:i:s.u)
Using the DateTime, DateTimeTz or Time type with microseconds
enabled columns can lead to errors because internally types expect
the exact format 'Y-m-d H:i:s' in combination with
``DateTime::createFromFormat()``. This method is twice a fast as
passing the date to the constructor of ``DateTime``.
This is why Doctrine always wants to create the time related types
without microseconds:
- DateTime to ``TIMESTAMP(0) WITHOUT TIME ZONE``
- DateTimeTz to ``TIMESTAMP(0) WITH TIME ZONE``
- Time to ``TIME(0) WITHOUT TIME ZONE``
If you do not let Doctrine create the date column types and rather
use types with microseconds you have replace the "DateTime",
"DateTimeTz" and "Time" types with a more liberal DateTime parser
that detects the format automatically:
::
use Doctrine\DBAL\Types\Type;
Type::overrideType('datetime', 'Doctrine\DBAL\Types\VarDateTime');
Type::overrideType('datetimetz', 'Doctrine\DBAL\Types\VarDateTime');
Type::overrideType('time', 'Doctrine\DBAL\Types\VarDateTime');
Timezones and DateTimeTz
~~~~~~~~~~~~~~~~~~~~~~~~
Postgres does not save the actual Timezone Name but UTC-Offsets.
The difference is subtle but can be potentially very nasty. Derick
Rethans explains it very well
`in a blog post of his <http://derickrethans.nl/storing-date-time-in-database.html>`_.
MySQL
-----
DateTimeTz
~~~~~~~~~~
MySQL does not support saving timezones or offsets. The DateTimeTz
type therefore behave like the DateTime type.
Sqlite
------
DateTimeTz
~~~~~~~~~~
Sqlite does not support saving timezones or offsets. The DateTimeTz
type therefore behave like the DateTime type.
IBM DB2
-------
DateTimeTz
~~~~~~~~~~
DB2 does not save the actual Timezone Name but UTC-Offsets. The
difference is subtle but can be potentially very nasty. Derick
Rethans explains it very well
`in a blog post of his <http://derickrethans.nl/storing-date-time-in-database.html>`_.
Oracle
------
DateTimeTz
~~~~~~~~~~
Oracle does not save the actual Timezone Name but UTC-Offsets. The
difference is subtle but can be potentially very nasty. Derick
Rethans explains it very well
`in a blog post of his <http://derickrethans.nl/storing-date-time-in-database.html>`_.
OCI8: SQL Queries with Question Marks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We had to implement a question mark to named parameter translation
inside the OCI8 DBAL Driver. It works as a very simple parser with two states: Inside Literal, Outside Literal.
From our perspective it should be working in all cases, but you have to be careful with certain
queries:
.. code-block:: sql
SELECT * FROM users WHERE name = 'bar?'
Could in case of a bug with the parser be rewritten into:
.. code-block:: sql
SELECT * FROM users WHERE name = 'bar:oci1'
For this reason you should always use prepared statements with
Oracle OCI8, never use string literals inside the queries. A query
for the user 'bar?' should look like:
.. code-block:: php
$sql = 'SELECT * FROM users WHERE name = ?'
$stmt = $conn->prepare($sql);
$stmt->bindValue(1, 'bar?');
$stmt->execute();
OCI-LOB instances
~~~~~~~~~~~~~~~~~
Doctrine 2 always requests CLOB columns as strings, so that you as
a developer never get access to the ``OCI-LOB`` instance. Since we
are using prepared statements for all write operations inside the
ORM, using strings instead of the ``OCI-LOB`` does not cause any
problems.
Microsoft SQL Server
--------------------
Unique and NULL
~~~~~~~~~~~~~~~
Microsoft SQL Server takes Unique very seriously. There is only
ever one NULL allowed contrary to the standard where you can have
multiple NULLs in a unique column.
Platforms
=========
Platforms abstract query generation and the subtle differences of
the supported database vendors. In most cases you don't need to
interact with the ``Doctrine\DBAL\Platforms`` package a lot, but
there might be certain cases when you are programming database
independent where you want to access the platform to generate
queries for you.
The platform can be accessed from any ``Doctrine\DBAL\Connection``
instance by calling the ``getDatabasePlatform()`` method.
::
<?php
$platform = $conn->getDatabasePlatform();
Each database driver has a platform associated with it by default.
Several drivers also share the same platform, for example PDO\_OCI
and OCI8 share the ``OraclePlatform``.
If you want to overwrite parts of your platform you can do so when
creating a connection. There is a ``platform`` option you can pass
an instance of the platform you want the connection to use:
::
<?php
$myPlatform = new MyPlatform();
$options = array(
'driver' => 'pdo_sqlite',
'path' => 'database.sqlite',
'platform' => $myPlatform
);
$conn = DriverManager::getConnection($options);
This way you can optimize your schema or generated SQL code with
features that might not be portable for instance, however are
required for your special needs. This can include using triggers or
views to simulate features or adding behaviour to existing SQL
functions.
Platforms are also responsible to know which database type
translates to which PHP Type. This is a very tricky issue across
all the different database vendors, for example MySQL BIGINT and
Oracle NUMBER should be handled as integer. Doctrine 2 offers a
powerful way to abstract the database to php and back conversion,
which is described in the next section.
Portability
===========
There are often cases when you need to write an application or library that is portable
across multiple different database vendors. The Doctrine ORM is one example of such
a library. It is an abstraction layer over all the currently supported vendors (MySQL, Oracle,
PostgreSQL, SQLite and MSSQL). If you want to use the DBAL to write a portable application
or library you have to follow lots of rules to make all the different vendors work the
same.
There are many different layers that you need to take care of, here is a quick list:
1. Returning of data is handled differently across vendors.
Oracle converts empty strings to NULL, which means a portable application
needs to convert all empty strings to null.
2. Additionally some vendors pad CHAR columns to their length, whereas others don't.
This means all strings returned from a database have to be passed through ``rtrim()``.
3. Case-sensitivity of column keys is handled differently in all databases, even depending
on identifier quoting or not. You either need to know all the rules or fix the cases
to lower/upper-case only.
4. ANSI-SQL is not implemented fully by the different vendors. You have to make
sure that the SQL you write is supported by all the vendors you are targeting.
5. Some vendors use sequences for identity generation, some auto-increment approaches.
Both are completely different (pre- and post-insert access) and therefore need
special handling.
6. Every vendor has a list of keywords that are not allowed inside SQL. Some even
allow a subset of their keywords, but not at every position.
7. Database types like dates, long text fields, booleans and many others are handled
very differently between the vendors.
8. There are differences with the regard to support of positional, named or both styles of parameters
in prepared statements between all vendors.
For each point in this list there are different abstraction layers in Doctrine DBAL that you
can use to write a portable application.
Connection Wrapper
------------------
This functionality is only implemented with Doctrine 2.1 upwards.
To handle all the points 1-3 you have to use a special wrapper around the database
connection. The handling and differences to tackle are all taken from the great
`PEAR MDB2 library <http://pear.php.net/package/MDB2/redirected>`_.
Using the following code block in your initialization will:
* ``rtrim()`` all strings if necessary
* Convert all empty strings to null
* Return all associative keys in lower-case, using PDO native functionality or implemented in PHP userland (OCI8).
.. code-block:: php
<?php
$params = array(
// vendor specific configuration
//...
'wrapperClass' => 'Doctrine\DBAL\Portability\Connection',
'portability' => \Doctrine\DBAL\Portability\Connection::PORTABILITY_ALL,
'fetch_case' => \PDO::CASE_LOWER,
);
This sort of portability handling is pretty expensive because all the result
rows and columns have to be looped inside PHP before being returned to you.
This is why by default Doctrine ORM does not use this compability wrapper but
implements another approach to handle assoc-key casing and ignores the other
two issues.
Database Platform
-----------------
Using the database platform you can generate bits of SQL for you, specifically
in the area of SQL functions to achieve portability. You should have a look
at all the different methods that the platforms allow you to access.
Keyword Lists
-------------
This functionality is only implemented with Doctrine 2.1 upwards.
Doctrine ships with lists of keywords for every supported vendor. You
can access a keyword list through the schema manager of the vendor you
are currently using or just instantiating it from the ``Doctrine\DBAL\Platforms\Keywords``
namespace.
\ No newline at end of file
SQL Query Builder
=================
Doctrine 2.1 ships with a powerful query builder for the SQL language. This QueryBuilder object has methods
to add parts to an SQL statement. If you built the complete state you can execute it using the connection
it was generated from. The API is roughly the same as that of the DQL Query Builder.
You can access the QueryBuilder by calling ``Doctrine\DBAL\Connection#createQueryBuilder``:
.. code-block:: php
<?php
$conn = DriverManager::getConnection(array(/*..*/));
$queryBuilder = $conn->createQueryBuilder();
Schema-Manager
==============
A Schema Manager instance helps you with the abstraction of the
generation of SQL assets such as Tables, Sequences, Foreign Keys
and Indexes.
To retrieve the ``SchemaManager`` for your connection you can use
the ``getSchemaManager()`` method:
.. code-block:: php
<?php
$sm = $conn->getSchemaManager();
Now with the ``SchemaManager`` instance in ``$em`` you can use the
available methods to learn about your database schema:
.. note::
Parameters containing identifiers passed to the SchemaManager
methods are *NOT* quoted automatically! Identifier quoting is
really difficult to do manually in a consistent way across
different databases. You have to manually quote the identifiers
when you accept data from user- or other sources not under your
control.
listDatabases()
---------------
Retrieve an array of databases on the configured connection:
.. code-block:: php
<?php
$databases = $sm->listDatabases();
listSequences()
-------------------------------
Retrieve an array of ``Doctrine\DBAL\Schema\Sequence`` instances
that exist for a database:
.. code-block:: php
<?php
$sequences = $sm->listSequences();
Or if you want to manually specify a database name:
.. code-block:: php
<?php
$sequences = $sm->listSequences('dbname');
Now you can loop over the array inspecting each sequence object:
.. code-block:: php
<?php
foreach ($sequences as $sequence) {
echo $sequence->getName() . "\n";
}
listTableColumns()
----------------------------
Retrieve an array of ``Doctrine\DBAL\Schema\Column`` instances that
exist for the given table:
.. code-block:: php
<?php
$columns = $sm->listTableColumns('user');
Now you can loop over the array inspecting each column object:
.. code-block:: php
<?php
foreach ($columns as $column) {
echo $column->getName() . ': ' . $column->getType() . "\n";
}
listTableDetails()
----------------------------
Retrieve a single ``Doctrine\DBAL\Schema\Table`` instance that
encapsulates all the details of the given table:
.. code-block:: php
<?php
$table = $sm->listTableDetails('user');
Now you can call methods on the table to manipulate the in memory
schema for that table. For example we can add a new column:
.. code-block:: php
<?php
$table->addColumn('email_address', 'string');
listTableForeignKeys()
--------------------------------
Retrieve an array of ``Doctrine\DBAL\Schema\ForeignKeyConstraint``
instances that exist for the given table:
.. code-block:: php
<?php
$foreignKeys = $sm->listTableForeignKeys('user');
Now you can loop over the array inspecting each foreign key
object:
.. code-block:: php
<?php
foreach ($foreignKeys as $foreignKey) {
echo $foreignKey->getName() . ': ' . $foreignKey->getLocalTableName() ."\n";
}
listTableIndexes()
----------------------------
Retrieve an array of ``Doctrine\DBAL\Schema\Index`` instances that
exist for the given table:
.. code-block:: php
<?php
$indexes = $sm->listTableIndexes('user');
Now you can loop over the array inspecting each index object:
.. code-block:: php
<?php
foreach ($indexes as $index) {
echo $index->getName() . ': ' . ($index->isUnique() ? 'unique' : 'not unique') . "\n";
}
listTables()
------------
Retrieve an array of ``Doctrine\DBAL\Schema\Table`` instances that
exist in the connections database:
.. code-block:: php
<?php
$tables = $sm->listTables();
Each ``Doctrine\DBAl\Schema\Table`` instance is populated with
information provided by all the above methods. So it encapsulates
an array of ``Doctrine\DBAL\Schema\Column`` instances that can be
retrieved with the ``getColumns()`` method:
.. code-block:: php
<?php
foreach ($tables as $table) {
echo $table->getName() . " columns:\n\n";
foreach ($table->getColumns() as $column) {
echo ' - ' . $column->getName() . "\n";
}
}
listViews()
-----------
Retrieve an array of ``Doctrine\DBAL\Schema\View`` instances that
exist in the connections database:
.. code-block:: php
<?php
$views = $sm->listViews();
Now you can loop over the array inspecting each view object:
.. code-block:: php
<?php
foreach ($views as $view) {
echo $view->getName() . ': ' . $view->getSql() . "\n";
}
createSchema()
--------------
For a complete representation of the current database you can use
the ``createSchema()`` method which returns an instance of
``Doctrine\DBAL\Schema\Schema``, which you can use in conjunction
with the SchemaTool or Schema Comparator.
.. code-block:: php
<?php
$fromSchema = $sm->createSchema();
Now we can clone the ``$fromSchema`` to ``$toSchema`` and drop a
table:
.. code-block:: php
<?php
$toSchema = clone $fromSchema;
$toSchema->dropTable('user');
Now we can compare the two schema instances in order to calculate
the differences between them and return the SQL required to make
the changes on the database:
.. code-block:: php
<?php
$sql = $fromSchema->getMigrateToSql($toSchema, $conn->getDatabasePlatform());
The ``$sql`` array should give you a SQL query to drop the user
table:
.. code-block:: php
<?php
print_r($sql);
/*
array(
0 => 'DROP TABLE user'
)
*/
Schema-Representation
=====================
Doctrine has a very powerful abstraction of database schemas. It
offers an object-oriented representation of a database schema with
support for all the details of Tables, Sequences, Indexes and
Foreign Keys. These Schema instances generate a representation that
is equal for all the supported platforms. Internally this
functionality is used by the ORM Schema Tool to offer you create,
drop and update database schema methods from your Doctrine ORM
Metadata model. Up to very specific functionality of your database
system this allows you to generate SQL code that makes your Domain
model work.
You will be pleased to hear, that Schema representation is
completly decoupled from the Doctrine ORM though, that is you can
also use it in any other project to implement database migrations
or for SQL schema generation for any metadata model that your
application has. You can easily generate a Schema, as a simple
example shows:
.. code-block:: php
<?php
$schema = new \Doctrine\DBAL\Schema\Schema();
$myTable = $schema->createTable("my_table");
$myTable->addColumn("id", "integer", array("unsigned" => true));
$myTable->addColumn("username", "string", array("length" => 32));
$myTable->setPrimaryKey(array("id"));
$myTable->addUniqueIndex(array("username"));
$schema->createSequence("my_table_seq");
$myForeign = $schema->createTable("my_foreign");
$myForeign->addColumn("id", "integer");
$myForeign->addColumn("user_id", "integer");
$myForeign->addForeignKeyConstraint($myTable, array("user_id"), array("id"), array("onUpdate" => "CASCADE"));
$queries = $schema->toSql($myPlatform); // get queries to create this schema.
$dropSchema = $schema->toDropSql($myPlatform); // get queries to safely delete this schema.
Now if you want to compare this schema with another schema, you can
use the ``Comparator`` class to get instances of ``SchemaDiff``,
``TableDiff`` and ``ColumnDiff``, as well as information about other
foreign key, sequence and index changes.
.. code-block:: php
<?php
$comparator = new \Doctrine\DBAL\Schema\Comparator();
$schemaDiff = $comparator->compare($fromSchema, $toSchema);
$queries = $schemaDiff->toSql($myPlatform); // queries to get from one to another schema.
$saveQueries = $schemaDiff->toSaveSql($myPlatform);
The Save Diff mode is a specific mode that prevents the deletion of
tables and sequences that might occour when making a diff of your
schema. This is often necessary when your target schema is not
complete but only describes a subset of your application.
All methods that generate SQL queries for you make much effort to
get the order of generation correct, so that no problems will ever
occour with missing links of foreign keys.
Security
========
Allowing users of your website to communicate with a database can possibly have security implications
that you should be aware of. Databases allow very powerful commands that not every user of your website
should be able to execute. Additionally the data in your database probably contains information that
should not be visible to everyone with access to the website.
The most dangerous security problem with regard to databases is the possibility of SQL injections.
An SQL injection security hole allows an attacker to execute new or modify existing SQL statements to
access information that he is not allowed to access.
Neither Doctrine DBAL nor ORM can prevent such attacks if you are careless as a developer. This section
explains to you the problems of SQL injection and how to prevent them.
User input in your queries
--------------------------
A database application necessarily requires user-input to passed to your queries.
There are wrong and right ways to do this and is very important to be very strict about this:
Wrong: String Concatenation
~~~~~~~~~~~~~~~~~~~~~~~~~~~
You should never ever build your queries dynamically and concatenate user-input into your
SQL or DQL query. For Example:
.. code-block:: php
<?php
// Very wrong!
$sql = "SELECT * FROM users WHERE name = '" . $_GET['username']. "'";
An attacker could inject any value into the GET variable "username" to modify the query to his needs.
Although DQL is a wrapper around SQL that can prevent you from some security implications, the previous
example is also a thread to DQL queries.
<?php
// DQL is not safe against arbitrary user-input as well:
$dql = "SELECT u FROM User u WHERE u.username = '" . $_GET['username'] . "'";
In this scenario an attacker could still pass a username set to "' OR 1 = 1" and create a valid DQL query.
Although DQL will make use of quoting functions when literals are used in a DQL statement, allowing
the attacker to modify the DQL statement with valid literals cannot be detected by the DQL parser, it
is your responsibility.
Right: Prepared Statements
~~~~~~~~~~~~~~~~~~~~~~~~~~
You should always use prepared statements to execute your queries. Prepared statements is a two-step
procedure, separating SQL query from the parameters. They are supported (and encouraged) for both
DBAL SQL queries and for ORM DQL queries.
Instead of using string concatenation to insert user-input into your SQL/DQL statements you just specify
either placeholders instead and then explain to the database driver which variable should be bound to
which placeholder. Each database vendor supports different placeholder styles:
- All PDO Drivers support positional (using question marks) and named placeholders (:param1, :foo, :bar).
- OCI8 only supports named parameters, but Doctrine DBAL has a thin layer around OCI8 and
also allows positional placeholders.
- Doctrine ORM DQL allows both named and positional parameters. The positional parameters however are not
just question marks, but suffixed with a number (?1, ?2, ?3, ...).
Following are examples of using prepared statements with SQL and DQL:
.. code-block:: php
<?php
// SQL Prepared Statements: Positional
$sql = "SELECT * FROM users WHERE username = ?";
$stmt = $connection->prepare($sql);
$stmt->bindValue(1, $_GET['username']);
$stmt->execute();
// SQL Prepared Statements: Named
$sql = "SELECT * FROM users WHERE username = :user";
$stmt = $connection->prepare($sql);
$stmt->bindValue("user", $_GET['username']);
$stmt->execute();
// DQL Prepared Statements: Positional
$dql = "SELECT u FROM User u WHERE u.username = ?1";
$query = $em->createQuery($dql);
$query->setParameter(1, $_GET['username']);
$data = $query->getResult();
// DQL Prepared Statements: Named
$dql = "SELECT u FROM User u WHERE u.username = :name";
$query = $em->createQuery($dql);
$query->setParameter("name", $_GET['username']);
$data = $query->getResult();
You can see this is a bit more tedious to write, but this is the only way to write secure queries. If you
are using just the DBAL there are also helper methods which simplify the usage quite alot:
.. code-block:: php
<?php
// bind parameters and execute query at once.
$sql = "SELECT * FROM users WHERE username = ?";
$stmt = $connection->executeQuery($sql, array($_GET['username']));
There is also ``executeUpdate`` which does not return a statement but the number of affected rows.
Besides binding parameters you can also pass the type of the variable. This allows Doctrine or the underyling
vendor to not only escape but also cast the value to the correct type. See the docs on querying and DQL in the
respective chapters for more information.
Right: Quoting/Escaping values
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Although previously we said string concatenation is wrong, there is a way to do it correctly using
the ``Connection#quote`` method:
.. code-block:: php
<?php
// Parameter quoting
$sql = "SELECT * FROM users WHERE name = " . $connection->quote($_GET['username'], \PDO::PARAM_STR);
This method is only available for SQL, not for DQL. For DQL it is always encouraged to use prepared
statements not only for security, but also for caching reasons.
Non-ASCII compatible Charsets in MySQL
--------------------------------------
Up until PHP 5.3.6 PDO has a security problem when using non ascii compatible charsets. Even if specifying
the charset using "SET NAMES", emulated prepared statements and ``PDO#quote`` could not reliably escape
values, opening up to potential SQL injections. If you are running PHP 5.3.6 you can solve this issue
by passing the driver option "charset" to Doctrine PDO MySQL driver. Using SET NAMES does not suffice!
.. code-block::
<?php
$conn = DriverManager::getConnection(array(
'driver' => 'pdo_mysql',
'charset' => 'UTF8',
));
Sharding
========
.. note::
The sharding extension is currently in transition from a seperate Project
into DBAL. Class names may differ.
Starting with 2.3 Doctrine DBAL contains some functionality to simplify the
development of horizontally sharded applications. In this first release it
contains a ``ShardManager`` interface. This interface allows to programatically
select a shard to send queries to. At the moment there are no functionalities
yet to dynamically pick a shard based on ID, query or database row yet. That
means the sharding extension is primarily suited for:
- multi-tenant applications or
- applications with completly separated datasets (example: weather data).
Both kind of application will work with both DBAL and ORM.
.. note::
Horizontal sharding is an evasive archicture that will affect your application code and using this
extension to Doctrine will not make it work "magically".
You have to understand and integrate the following drawbacks:
- Pre-generation of IDs that are unique across all shards required.
- No transaction support across shards.
- No foreign key support across shards (meaning no "real" relations).
- Very complex (or impossible) to query aggregates across shards.
- Denormalization: Composite keys required where normalized non-sharded db schemas don't need them.
- Schema Operations have to be done on all shards.
The primary questions in a sharding architecture are:
* Where is my data located?
* Where should I save this new data to find it later?
To answer these questions you generally have to craft a function that will tell
you for a given ID, on which shard the data for this ID is located. To simplify
this approach you will generally just pick a table which is the root of a set of
related data and decide for the IDs of this table. All the related data that
belong to this table are saved on the same shard.
Take for example a multi-user blog application with the following tables:
- Blog [id, name]
- Post [id, blog_id, subject, body, author_id]
- Comment [id, post_id, comment, author_id]
- User [id, username]
A sensible sharding architecture will split the application by blog. That means
all the data for a particular blog will be on a single shard and scaling is
done by putting the amound of blogs on many different database servers.
Now users can post and comment on different blogs that reside on different
shards. This makes the database schema above slightly tricky, because both
`author_id` columns cannot have foreign keys to `User (id)`. Instead the User
table is located in an entirely different "dimension" of the application in
terms of the sharding architecture.
To simplify working with this kind of multi-dimensional database schema, you
can replace the author_ids with something more "meaningful", for example the
e-mail address of the users if that is always known. The "user" table can then
be seperated from the database schema above and put on a second horizontally
scaled sharding architecture.
As you can see, even with just the four tables above, sharding actually becomes
quite complex to think about.
The rest of this section discusses Doctrine sharding functionality in technical
detail.
ID Generation
-------------
To solve the issue of unique ID-generation across all shards are several
approaches you should evaluate:
Use GUID/UUIDs
~~~~~~~~~~~~~~
The most simple ID-generation mechanism for sharding are
universally unique identifiers. These are 16-byte
(128-bit) numbers that are guaranteed to be unique across different servers.
You can `read up on UUIDs on Wikipedia
<http://en.wikipedia.org/wiki/Universally_unique_identifier>`_.
The drawback of UUIDs is the segmentation they cause on indexes. Because UUIDs
are not sequentially generated, they can have negative impact on index access
performance. Additionally they are much bigger
than numerical primary keys (which are normally 4-bytes in length).
At the moment Doctrine DBAL drivers MySQL and SQL Server support the generation
of UUID/GUIDs. You can use the following bit of code to generate them across
platforms:
.. code-block:: php
<?php
use Doctrine\DBAL\DriverManager;
$conn = DriverManager::getConnection(/**..**/);
$guid = $conn->fetchColumn('SELECT ' . $conn->getDatabasePlatform()->getGuidExpression());
$conn->insert("my_table", array("id" => $guid, "foo" => "bar"));
In your application you should hide this details in Id-Generation services:
.. code-block:: php
<?php
namespace MyApplication;
class IdGenerationService
{
private $conn;
public function generateCustomerId()
{
return $this->conn->fetchColumn('SELECT ' .
$this->conn->getDatabasePlatform()->getGuidExpression()
);
}
}
A good starting point to read up on GUIDs (vs numerical ids) is this blog post
`Coding Horror: Primary Keys: IDs vs GUIDs
<http://www.codinghorror.com/blog/2007/03/primary-keys-ids-versus-guids.html>`_.
Table Generator
~~~~~~~~~~~~~~~
In some scenarios there is no way around a numerical, automatically
incrementing id. The way Auto incrementing IDs are implemented in MySQL and SQL
Server however is completly unsuitable for sharding. Remember in a sharding
architecture you have to know where the row for a specific ID is located and
IDs have to be globally unique across all servers. Auto-Increment Primary Keys
are missing both properties.
To get around this issue you can use the so-called "table-generator" strategy.
In this case you define a single database that is responsible for the
generation of auto-incremented ids. You create a table on this database and
through the use of locking create new sequential ids.
There are three important drawbacks to this strategy:
- Single point of failure
- Bottleneck when application is write-heavy
- A second independent database connection is needed to guarantee transaction
safety.
If you can live with this drawbacks then you can use table-generation with the
following code in Doctrine:
.. code-block:: php
<?php
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Id\TableGenerator;
$conn = DriverManager::getConnection(/**..**); // connection 1
// creating the TableGenerator automatically opens a second connection.
$tableGenerator = new TableGenerator($conn, "sequences_tbl_name");
$id1 = $tableGenerator->nextValue("sequence_name1");
$id2 = $tableGenerator->nextValue("sequence_name2");
The table generator obviously needs a table to work. The schema of this table
is described in the ``TableGenerator`` class-docblock. Alternatively you
can use the ``Doctrine\DBAL\Id\TableGeneratorSchemaVisitor`` and apply it to your
``Doctrine\DBAL\Schema\Schema`` instance. It will automatically add the required
sequence table.
Natural Identifiers
~~~~~~~~~~~~~~~~~~~
Sometimes you are lucky and your application data-model comes with a natural
id. This is mostly the case for applications who get their IDs generated
somewhere else (exogeneous ID-generation) or that work with temporal data. In
that case you can just define the natural primary key and shard your
application based on this data.
Transactions
------------
Transactions in sharding can only work for data that is located on a single
shard. If you need transactions in your sharding architecture then you have to
make sure that the data updated during a transaction is located on a single
shard.
Foreign Keys
------------
Since you cannot create foreign keys between remote database servers, in a
sharding architecture you should put the data on a shard that belongs to each
other. But even if you can isolate most of the rows on a single shard there may
exist relations between tables that exist on different shards. In this case
your application should be aware of the potential inconsistencies and handle
them graciously.
Complex Queries
---------------
GROUP BY, DISTINCT and ORDER BY are clauses that cannot be easily used in a
sharding architecture. If you have to execute these queries against multiple
shards then you cannot just append the different results to each other.
You have to be aware of this problem and design your queries accordingly or
shard the data in a way that you never have to query multiple shards to
calculate a result.
ShardManager Interface
----------------------
The central API of the sharding extension is the ``ShardManager`` interface.
It contains two different groups of functions with regard to sharding.
First, it contains the Shard Selection API. You can pick a shard based on a
so-called "distribution-value" or reset the connection to the "global" shard,
a necessary database that often contains heavily cached, sharding independent
data such as meta tables or the "user/tenant" table.
.. code-block:: php
<?php
use Doctrine\DBAL\DriverManager;
use Doctrine\Shards\DBAL\SQLAzure\SQLAzureShardManager;
$conn = DriverManager::getConnection(array(
'sharding' => array(
'federationName' => 'my_database',
'distributionKey' => 'customer_id',
)
));
$shardManager = new SQLAzureShardManager($conn);
$currentCustomerId = 1234;
$shardManager->selectShard($currentCustomerId);
// all queries after this call hit the shard
// where customer with id 1234 is on.
$shardManager->selectGlobal();
// the global database is selected.
To access the currently selected distribution value use the following API
method:
.. code-block:: php
<?php
$value = $shardManager->getCurrentDistributionValue();
The shard manager will prevent you switching shards when a transaction is open.
This is especially important when using sharding with the ORM. Because the ORM
uses a single transaction during the flush-operation this means that you can
only ever use one ``EntityManager`` with data from a single shard.
The second API is the "fan-out" query API. This allows you to execute queries against
ALL shards. The order of the results of this operation is undefined, that means
your query has to return the data in a way that works for the application, or
you have to sort the data in the application.
.. code-block:: php
<?php
$sql = "SELECT * FROM customers";
$rows = $shardManager->queryAll($sql, $params);
Schema Operations: SchemaSynchronizer Interface
-----------------------------------------------
Schema Operations in a sharding architecture are tricky. You have to perform
them on all databases instances (shards) at the same time. Also Doctrine
has problems with this in particular as you cannot generate an SQL file with
changes on any development machine anymore and apply this on production. The
required changes depend on the amount of shards.
To allow the Doctrine Schema API operations on a sharding architecture we
performed a refactored from code inside ORM ``Doctrine\ORM\Tools\SchemaTool``
class and extracted the code for operations on Schema instances into a new
``Doctrine\Shards\DBAL\SchemaSynchronizer`` interface.
Every sharding implementation can implement this interface and allow schema
operations to take part on multiple shards.
SQL Azure Federations
---------------------
Doctrine Shards ships with a custom implementation for Microsoft SQL
Azure. The Azure platform provides a native sharding functionality. In SQL
Azure the sharding functionality is called Federations. This
functionality applies the following restrictions (in line with the ones listed
above):
- IDENTITY columns are not allowed on sharded tables (federated tables)
- Each table may only have exactly one clustered index and this index has to
have the distribution key/sharding-id as one column.
- Every unique index (or primary key) has to contain the
distribution-key/sharding-id.
Especially the requirements 2 and 3 prevent normalized database schemas. You
have to put the distribution key on every sharded table, which can affect your
application code quite a bit. This may lead to the creation of composite keys
where you normally wouldn't need them.
The benefit of SQL Azure Federations is that they implement all the
shard-picking logic on the server. You only have to make use of the ``USE
FEDERATION`` statement. You don't have to maintain a list of all the shards
inside your application and more importantly, resizing shards is done
transparently on the server.
Features of SQL Azure are:
- Central server to log into federations architecture. No need to know all
connection details of all shards.
- Database level operation to split shards, taking away the tediousness of this
operation for application developers.
- A global tablespace that can contain global data to all shards.
- One or many different federations (this library only supports working with
one)
- Sharded or non-sharded tables inside federations
- Allows filtering SELECT queries on the database based on the selected
sharding key value. This allows to implement sharded Multi-Tenant Apps very easily.
To setup an SQL Azure ShardManager use the following code:
.. code-block:: php
<?php
use Doctrine\DBAL\DriverManager;
use Doctrine\Shards\DBAL\SQLAzure\SQLAzureShardManager;
$conn = DriverManager::getConnection(array(
'dbname' => 'my_database',
'host' => 'tcp:dbname.windows.net',
'user' => 'user@dbname',
'password' => 'XXX',
'sharding' => array(
'federationName' => 'my_federation',
'distributionKey' => 'customer_id',
'distributionType' => 'integer',
)
));
$shardManager = new SQLAzureShardManager($conn);
Currently you are limited to one federation in your application.
You can inspect all the currently known shards on SQL Azure using the
``ShardManager#getShards()`` function:
.. code-block:: php
<?php
foreach ($shardManager->getShards() as $shard) {
echo $shard['id'] . " " . $shard['rangeLow'] . " - " . $shard['rangeHigh'];
}
Schema Operations
~~~~~~~~~~~~~~~~~
Schema Operations on SQL Azure Federations are possible with the
``SQLAzureSchemaSynchronizer``. You can instantiate this from your code:
.. code-block:: php
<?php
use Doctrine\Shards\DBAL\SQLAzure\SQLAzureSchemaSynchronizer;
$synchronizer = new SQLAzureSchemaSynchronizer($conn, $shardManager);
You can use the API such as ``createSchema($schema)`` then and it will be
distributed across all shards. The assumptions are:
- Using ``SchemaSynchronizer#createSchema()`` assumes the database is empty.
The federation is created during this operation.
- Using ``SchemaSynchronizer#updateSchema()`` assumes the database and the
federation exists. All shards of the federation are iterated and update is
applied to all shards consecutively.
For a schema with tables in the global or federated sub-schema you have to use
the Schema API to mark tables:
.. code-block:: php
<?php
use Doctrine\DBAL\Schema\Schema;
$schema = new Schema();
// no options set, this table will be on the federation root
$users = $schema->createTable('Users');
//...
// marked as sharded, but no distribution column given:
// non-federated table inside the federation
$products = $schema->createTable('Products');
$products->addOption('azure.federated', true);
//...
// shared + distribution column:
// federated table
$customers = $schema->createTable('Customers');
$customers->addColumn('CustomerID', 'integer');
//...
$customers->addOption('azure.federated', true);
$customers->addOption('azure.federatedOnColumnName', 'CustomerID');
SQLAzure Filtering
~~~~~~~~~~~~~~~~~~
SQL Azure comes with a powerful filtering feature, that allows you to
automatically implement a multi-tenant application for a formerly single-tenant
application. The restriction to make this work is that your application does not work with
IDENTITY columns.
Normally when you select a shard using ``ShardManager#selectShard()`` any query
executed against this shard will return data from ALL the tenants located on
this shard. With the "FILTERING=ON" flag on the ``USE FEDERATION`` query
however SQL Azure can automatically filter all SELECT queries with the chosen
distribution value. Additionally you can automatically set the currently
selected distribution value in every INSERT statement using a function for this
value as the ``DEFAULT`` part of the column. If you are using GUIDs for every
row then UPDATE and DELETE statements using only GUIDs will work out perfectly
as well, as they are by definition for unique rows. This feature allows you to
build multi-tenant applications, even though they were not originally designed
that way.
To enable filtering you can use the
``SQLAzureShardManager#setFilteringEnabled()`` method. This method is not part
of the interface. You can also set a default value for filtering by passing it
as the "sharding.filteringEnabled" parameter to
``DriverManager#getConnection()``.
Generic SQL Sharding Support
----------------------------
Besides the custom SQL Azure support there is a generic implementation that
works with all database drivers. It requires to specify all database
connections and will switch between the different connections under the hood
when using the ``ShardManager`` API. This is also the biggest drawback of this
approach, since fan-out queries need to connect to all databases in a single
request.
See the configuration for a sample sharding connection:
.. code-block:: php
<?php
use Doctrine\DBAL\DriverManager;
$conn = DriverManager::getConnection(array(
'wrapperClass' => 'Doctrine\Shards\DBAL\PoolingShardConnection',
'driver' => 'pdo_sqlite',
'global' => array('memory' => true),
'shards' => array(
array('id' => 1, 'memory' => true),
array('id' => 2, 'memory' => true),
),
'shardChoser' => 'Doctrine\Shards\DBAL\ShardChoser\MultiTenantShardChoser',
));
You have to configure the following options:
- 'wrapperClass' - Selecting the PoolingShardConnection as above.
- 'global' - An array of database parameters that is used for connecting to the
global database.
- 'shards' - An array of of shard database parameters. You have to specifiy an
'id' parameter for each of the shard configurations.
- 'shardChoser' - Implementation of the
``Doctrine\Shards\DBAL\ShardChoser\ShardChoser`` interface.
The Shard Choser interface maps the distribution value to a shard-id. This
gives you the freedom to implement your own strategy for sharding the data
horizontally.
SQLAzure Sharding Tutorial
==========================
.. note::
The sharding extension is currently in transition from a seperate Project
into DBAL. Class names may differ.
This tutorial builds upon the `Brian Swans tutorial
<http://blogs.msdn.com/b/silverlining/archive/2012/01/18/using-sql-azure-federations-via-php.aspx>`_
on SQLAzure Sharding and turns all the examples into examples using the Doctrine Sharding support.
It introduces SQL Azure Sharding, which is an abstraction layer in SQL Azure to
support sharding. Many features for sharding are implemented on the database
level, which makes it much easier to work with than generic sharding
implementations.
For this tutorial you need an Azure account. You don't need to deploy the code
on Azure, you can run it from your own machine against the remote database.
.. note::
You can look at the code from the 'examples/sharding' directory.
Install Doctrine
----------------
For this tutorial we will install Doctrine and the Sharding Extension through
`Composer <http://getcomposer.org>`_ which is the easiest way to install
Doctrine. Composer is a new package manager for PHP. Download the
``composer.phar`` from their website and put it into a newly created folder for
this tutorial. Now create a ``composer.json`` file in this project root with
the following content:
{
"require": {
"doctrine/dbal": "2.2.2",
"doctrine/shards": "0.2"
}
}
Open up the commandline and switch to your tutorial root directory, then call
``php composer.phar install``. It will grab the code and install it into the
``vendor`` subdirectory of your project. It also creates an autoloader, so that
we don't have to care about this.
Setup Connection
----------------
The first thing to start with is setting up Doctrine and the database connection:
.. code-block:: php
<?php
// bootstrap.php
use Doctrine\DBAL\DriverManager;
use Doctrine\Shards\DBAL\SQLAzure\SQLAzureShardManager;
require_once "vendor/autoload.php";
$conn = DriverManager::getConnection(array(
'driver' => 'pdo_sqlsrv',
'dbname' => 'SalesDB',
'host' => 'tcp:dbname.windows.net',
'user' => 'user@dbname',
'password' => 'XXX',
'platform' => new \Doctrine\DBAL\Platforms\SQLAzurePlatform(),
'driverOptions' => array('MultipleActiveResultSets' => false),
'sharding' => array(
'federationName' => 'Orders_Federation',
'distributionKey' => 'CustId',
'distributionType' => 'integer',
)
));
$shardManager = new SQLAzureShardManager($conn);
Create Database
---------------
Create a new database using the Azure/SQL Azure management console.
Create Schema
-------------
Doctrine has a powerful schema API. We don't need to use low-level DDL
statements to generate the database schema. Instead you can use an Object-Oriented API
to create the database schema and then have Doctrine turn it into DDL
statements.
We will recreate Brians example schema with Doctrine DBAL. Instead of having to
create federations and schema seperately as in his example, Doctrine will do it
all in one step:
.. code-block:: php
<?php
// create_schema.php
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Shards\DBAL\SQLAzure\SQLAzureSchemaSynchronizer;
require_once 'bootstrap.php';
$schema = new Schema();
$products = $schema->createTable('Products');
$products->addColumn('ProductID', 'integer');
$products->addColumn('SupplierID', 'integer');
$products->addColumn('ProductName', 'string');
$products->addColumn('Price', 'decimal', array('scale' => 2, 'precision' => 12));
$products->setPrimaryKey(array('ProductID'));
$products->addOption('azure.federated', true);
$customers = $schema->createTable('Customers');
$customers->addColumn('CustomerID', 'integer');
$customers->addColumn('CompanyName', 'string');
$customers->addColumn('FirstName', 'string');
$customers->addColumn('LastName', 'string');
$customers->setPrimaryKey(array('CustomerID'));
$customers->addOption('azure.federated', true);
$customers->addOption('azure.federatedOnColumnName', 'CustomerID');
$orders = $schema->createTable('Orders');
$orders->addColumn('CustomerID', 'integer');
$orders->addColumn('OrderID', 'integer');
$orders->addColumn('OrderDate', 'datetime');
$orders->setPrimaryKey(array('CustomerID', 'OrderID'));
$orders->addOption('azure.federated', true);
$orders->addOption('azure.federatedOnColumnName', 'CustomerID');
$orderItems = $schema->createTable('OrderItems');
$orderItems->addColumn('CustomerID', 'integer');
$orderItems->addColumn('OrderID', 'integer');
$orderItems->addColumn('ProductID', 'integer');
$orderItems->addColumn('Quantity', 'integer');
$orderItems->setPrimaryKey(array('CustomerID', 'OrderID', 'ProductID'));
$orderItems->addOption('azure.federated', true);
$orderItems->addOption('azure.federatedOnColumnName', 'CustomerID');
// Create the Schema + Federation:
$synchronizer = new SQLAzureSchemaSynchronizer($conn, $shardManager);
$synchronizer->createSchema($schema);
// Or jut look at the SQL:
echo implode("\n", $synchronizer->getCreateSchema($schema));
View Federation Members
-----------------------
To see how many shard instances (called Federation Members) your SQLAzure database currently has
you can ask the ``ShardManager`` to enumerate all shards:
.. code-block:: php
<?php
// view_federation_members.php
require_once "bootstrap.php";
$shards = $shardManager->getShards();
foreach ($shards as $shard) {
print_r($shard);
}
Insert Data
-----------
Now we want to insert some test data into the database to see the behavior when
we split the shards. We use the same test data as Brian, but use the Doctrine
API to insert them. To insert data into federated tables we have to select the
shard we want to put the data into. We can use the ShardManager to execute this
operation for us:
.. code-block:: php
<?php
// insert_data.php
require_once "bootstrap.php";
$shardManager->selectShard(0);
$conn->insert("Products", array(
"ProductID" => 386,
"SupplierID" => 1001,
"ProductName" => 'Titanium Extension Bracket Left Hand',
"Price" => 5.25,
));
$conn->insert("Products", array(
"ProductID" => 387,
"SupplierID" => 1001,
"ProductName" => 'Titanium Extension Bracket Right Hand',
"Price" => 5.25,
));
$conn->insert("Products", array(
"ProductID" => 388,
"SupplierID" => 1001,
"ProductName" => 'Fusion Generator Module 5 kV',
"Price" => 10.50,
));
$conn->insert("Products", array(
"ProductID" => 388,
"SupplierID" => 1001,
"ProductName" => 'Bypass Filter 400 MHz Low Pass',
"Price" => 10.50,
));
$conn->insert("Customers", array(
'CustomerID' => 10,
'CompanyName' => 'Van Nuys',
'FirstName' => 'Catherine',
'LastName' => 'Abel',
));
$conn->insert("Customers", array(
'CustomerID' => 20,
'CompanyName' => 'Abercrombie',
'FirstName' => 'Kim',
'LastName' => 'Branch',
));
$conn->insert("Customers", array(
'CustomerID' => 30,
'CompanyName' => 'Contoso',
'FirstName' => 'Frances',
'LastName' => 'Adams',
));
$conn->insert("Customers", array(
'CustomerID' => 40,
'CompanyName' => 'A. Datum Corporation',
'FirstName' => 'Mark',
'LastName' => 'Harrington',
));
$conn->insert("Customers", array(
'CustomerID' => 50,
'CompanyName' => 'Adventure Works',
'FirstName' => 'Keith',
'LastName' => 'Harris',
));
$conn->insert("Customers", array(
'CustomerID' => 60,
'CompanyName' => 'Alpine Ski House',
'FirstName' => 'Wilson',
'LastName' => 'Pais',
));
$conn->insert("Customers", array(
'CustomerID' => 70,
'CompanyName' => 'Baldwin Museum of Science',
'FirstName' => 'Roger',
'LastName' => 'Harui',
));
$conn->insert("Customers", array(
'CustomerID' => 80,
'CompanyName' => 'Blue Yonder Airlines',
'FirstName' => 'Pilar',
'LastName' => 'Pinilla',
));
$conn->insert("Customers", array(
'CustomerID' => 90,
'CompanyName' => 'City Power & Light',
'FirstName' => 'Kari',
'LastName' => 'Hensien',
));
$conn->insert("Customers", array(
'CustomerID' => 100,
'CompanyName' => 'Coho Winery',
'FirstName' => 'Peter',
'LastName' => 'Brehm',
));
$conn->executeUpdate("DECLARE @orderId INT
DECLARE @customerId INT
SET @orderId = 10
SELECT @customerId = CustomerId FROM Customers WHERE LastName = 'Hensien' and FirstName = 'Kari'
INSERT INTO Orders (CustomerId, OrderId, OrderDate)
VALUES (@customerId, @orderId, GetDate())
INSERT INTO OrderItems (CustomerID, OrderID, ProductID, Quantity)
VALUES (@customerId, @orderId, 388, 4)
SET @orderId = 20
SELECT @customerId = CustomerId FROM Customers WHERE LastName = 'Harui' and FirstName = 'Roger'
INSERT INTO Orders (CustomerId, OrderId, OrderDate)
VALUES (@customerId, @orderId, GetDate())
INSERT INTO OrderItems (CustomerID, OrderID, ProductID, Quantity)
VALUES (@customerId, @orderId, 389, 2)
SET @orderId = 30
SELECT @customerId = CustomerId FROM Customers WHERE LastName = 'Brehm' and FirstName = 'Peter'
INSERT INTO Orders (CustomerId, OrderId, OrderDate)
VALUES (@customerId, @orderId, GetDate())
INSERT INTO OrderItems (CustomerID, OrderID, ProductID, Quantity)
VALUES (@customerId, @orderId, 387, 3)
SET @orderId = 40
SELECT @customerId = CustomerId FROM Customers WHERE LastName = 'Pais' and FirstName = 'Wilson'
INSERT INTO Orders (CustomerId, OrderId, OrderDate)
VALUES (@customerId, @orderId, GetDate())
INSERT INTO OrderItems (CustomerID, OrderID, ProductID, Quantity)
VALUES (@customerId, @orderId, 388, 1)"
);
This puts the data into the currently only existing federation member. We
selected that federation member by picking 0 as distribution value, which is by
definition part of the only existing federation.
Split Federation
----------------
Now lets split the federation, creating a second federation member. SQL Azure
will automatically redistribute the data into the two federations after you
executed this command.
.. code-block:: php
<?php
// split_federation.php
require_once 'bootstrap.php';
$shardManager->splitFederation(60);
This little script uses the shard manager with a special method only existing
on the SQL AZure implementation ``splitFederation``. It accepts a value at
at which the split is executed.
If you reexecute the ``view_federation_members.php`` script you can now see
that there are two federation members instead of just one as before. You can
see with the ``rangeLow`` and ``rangeHigh`` parameters what customers and
related entries are now served by which federation.
Inserting Data after Split
--------------------------
Now after we splitted the data we now have to make sure to be connected to the
right federation before inserting data. Lets add a new customer with ID 55 and
have him create an order.
.. code-block:: php
<?php
// insert_data_aftersplit.php
require_once 'bootstrap.php';
$newCustomerId = 55;
$shardManager->selectShard($newCustomerId);
$conn->insert("Customers", array(
"CustomerID" => $newCustomerId,
"CompanyName" => "Microsoft",
"FirstName" => "Brian",
"LastName" => "Swan",
));
$conn->insert("Orders", array(
"CustomerID" => 55,
"OrderID" => 37,
"OrderDate" => date('Y-m-d H:i:s'),
));
$conn->insert("OrderItems", array(
"CustomerID" => 55,
"OrderID" => 37,
"ProductID" => 387,
"Quantity" => 1,
));
As you can see its very important to pick the right distribution key in your
sharded application. Otherwise you have to switch the shards very often, which
is not really easy to work with. If you pick the sharding key right then it
should be possible to select the shard only once per request for the major
number of use-cases.
Fan-out the queries accross multiple shards should only be necessary for a
small number of queries, because these kind of queries are complex.
Querying data with filtering off
--------------------------------
To access the data you have to pick a shard again and then start selecting data
from it.
.. code-block:: php
<?php
// query_filtering_off.php
require_once "bootstrap.php";
$shardManager->selectShard(0);
$data = $conn->fetchAll('SELECT * FROM Customers');
print_r($data);
This returns all customers from the shard with distribution value 0. This will
be all customers with id 10 to less than 60, since we split federations at 60.
Querying data with filtering on
-------------------------------
One special feature of SQL Azure is the possibility to database level filtering
based on the sharding distribution values. This means that SQL Azure will add
WHERE clauses with distributionkey=current distribution value conditions to
each distribution key.
.. code-block:: php
<?php
// query_filtering_on.php
require_once "bootstrap.php";
$shardManager->setFilteringEnabled(true);
$shardManager->selectShard(55);
$data = $conn->fetchAll('SELECT * FROM Customers');
print_r($data);
Now you only get the customer with id = 55. The same holds for queries on the
``Orders`` and ``OrderItems`` table, which are restricted by customer id = 55.
Supporting Other Databases
==========================
To support a database which is not currently shipped with Doctrine
you have to implement the following interfaces and abstract
classes:
- ``\Doctrine\DBAL\Driver\Driver``
- ``\Doctrine\DBAL\Driver\Statement``
- ``\Doctrine\DBAL\Platforms\AbstractPlatform``
- ``\Doctrine\DBAL\Schema\AbstractSchemaManager``
For an already supported platform but unsupported driver you only
need to implement the first two interfaces, since the SQL
Generation and Schema Management is already supported by the
respective platform and schema instances. You can also make use of
several Abstract Unittests in the ``\Doctrine\Tests\DBAL`` package
to check if your platform behaves like all the others which is
necessary for SchemaTool support, namely:
- ``\Doctrine\Tests\DBAL\Platforms\AbstractPlatformTestCase``
- ``\Doctrine\Tests\DBAL\Functional\Schema\AbstractSchemaManagerTestCase``
We would be very happy if any support for new databases would be
contributed back to Doctrine to make it an even better product.
Implementation Steps in Detail
------------------------------
1. Add your driver shortcut to class-name `Doctrine\DBAL\DriverManager`.
2. Make a copy of tests/dbproperties.xml.dev and adjust the values to your driver shortcut and testdatabase.
3. Create three new classes implementing ``\Doctrine\DBAL\Driver\Driver``, ``\Doctrine\DBAL\Driver\Statement``
and ``Doctrine\DBAL\Driver``. You can take a look at the ``Doctrine\DBAL\Driver\OCI8`` driver.
4. You can run the testsuite of your new database driver by calling "cd tests/ && phpunit -c myconfig.xml Doctrine/Tess/AllTests.php"
5. Start implementing AbstractPlatform and AbstractSchemaManager. Other implementations should serve as good example.
\ No newline at end of file
Transactions
============
A ``Doctrine\DBAL\Connection`` provides a PDO-like API for
transaction management, with the methods
``Connection#beginTransaction()``, ``Connection#commit()`` and
``Connection#rollback()``.
Transaction demarcation with the Doctrine DBAL looks as follows:
::
<?php
$conn->beginTransaction();
try{
// do stuff
$conn->commit();
} catch(Exception $e) {
$conn->rollback();
throw $e;
}
Alternatively, the control abstraction
``Connection#transactional($func)`` can be used to make the code
more concise and to make sure you never forget to rollback the
transaction in the case of an exception. The following code snippet
is functionally equivalent to the previous one:
::
<?php
$conn->transactional(function($conn) {
// do stuff
});
The ``Doctrine\DBAL\Connection`` also has methods to control the
transaction isolation level as supported by the underlying
database. ``Connection#setTransactionIsolation($level)`` and
``Connection#getTransactionIsolation()`` can be used for that purpose.
The possible isolation levels are represented by the following
constants:
::
<?php
Connection::TRANSACTION_READ_UNCOMMITTED
Connection::TRANSACTION_READ_COMMITTED
Connection::TRANSACTION_REPEATABLE_READ
Connection::TRANSACTION_SERIALIZABLE
The default transaction isolation level of a
``Doctrine\DBAL\Connection`` is chosen by the underlying platform
but it is always at least READ\_COMMITTED.
Transaction Nesting
-------------------
A ``Doctrine\DBAL\Connection`` also adds support for nesting
transactions, or rather propagating transaction control up the call
stack. For that purpose, the ``Connection`` class keeps an internal
counter that represents the nesting level and is
increased/decreased as ``beginTransaction()``, ``commit()`` and
``rollback()`` are invoked. ``beginTransaction()`` increases the
nesting level whilst
``commit()`` and ``rollback()`` decrease the nesting level. The nesting level starts at 0. Whenever the nesting level transitions from 0 to 1, ``beginTransaction()`` is invoked on the underlying driver connection and whenever the nesting level transitions from 1 to 0, ``commit()`` or ``rollback()`` is invoked on the underlying driver, depending on whether the transition was caused by ``Connection#commit()`` or ``Connection#rollback()``.
What this means is that transaction control is basically passed to
code higher up in the call stack and the inner transaction block is
ignored, with one important exception that is described further
below. Do not confuse this with "real" nested transactions or
savepoints. These are not supported by Doctrine. There is always
only a single, real database transaction.
To visualize what this means in practice, consider the following
example:
::
<?php
// $conn instanceof Doctrine\DBAL\Connection
$conn->beginTransaction(); // 0 => 1, "real" transaction started
try {
...
// nested transaction block, this might be in some other API/library code that is
// unaware of the outer transaction.
$conn->beginTransaction(); // 1 => 2
try {
...
$conn->commit(); // 2 => 1
} catch (Exception $e) {
$conn->rollback(); // 2 => 1, transaction marked for rollback only
throw $e;
}
...
$conn->commit(); // 1 => 0, "real" transaction committed
} catch (Exception $e) {
$conn->rollback(); // 1 => 0, "real" transaction rollback
throw $e;
}
However,
**a rollback in a nested transaction block will always mark the current transaction so that the only possible outcome of the transaction is to be rolled back**.
That means in the above example, the rollback in the inner
transaction block marks the whole transaction for rollback only.
Even if the nested transaction block would not rethrow the
exception, the transaction is marked for rollback only and the
commit of the outer transaction would trigger an exception, leading
to the final rollback. This also means that you can not
successfully commit some changes in an outer transaction if an
inner transaction block fails and issues a rollback, even if this
would be the desired behavior (i.e. because the nested operation is
"optional" for the purpose of the outer transaction block). To
achieve that, you need to restructure your application logic so as
to avoid nesting transaction blocks. If this is not possible
because the nested transaction blocks are in a third-party API
you're out of luck.
All that is guaruanteed to the inner transaction is that it still
happens atomically, all or nothing, the transaction just gets a
wider scope and the control is handed to the outer scope.
.. note::
The transaction nesting described here is a debated
feature that has its critics. Form your own opinion. We recommend
avoiding nesting transaction blocks when possible, and most of the
time, it is possible. Transaction control should mostly be left to
a service layer and not be handled in data access objects or
similar.
.. warning::
Directly invoking ``PDO#beginTransaction()``,
``PDO#commit()`` or ``PDO#rollback()`` or the corresponding methods
on the particular ``Doctrine\DBAL\Driver\Connection`` instance in
use bypasses the transparent transaction nesting that is provided
by ``Doctrine\DBAL\Connection`` and can therefore corrupt the
nesting level, causing errors with broken transaction boundaries
that may be hard to debug.
Types
=====
Besides abstraction of SQL one needs a translation between database
and PHP data-types to implement database independent applications.
Doctrine 2 has a type translation system baked in that supports the
conversion from and to PHP values from any database platform,
as well as platform independent SQL generation for any Doctrine
Type.
Using the ORM you generally don't need to know about the Type
system. This is unless you want to make use of database vendor
specific database types not included in Doctrine 2. The following
PHP Types are abstracted across all the supported database
vendors:
- Integer
- SmallInt
- BigInt
- String (string with maximum length, for example 255)
- Text (strings without maximum length)
- Decimal (restricted floats, *NOTE* Only works with a setlocale()
configuration that uses decimal points!)
- Boolean
- DateTime
- Date (DateTime instance where only Y-m-d get persisted)
- Time (DateTime instance where only H:i:s get persisted)
- Array (serialized into a text field for all vendors by default)
- Object (serialized into a text field for all vendors by default)
- Float (*NOTE* Only works with a setlocale() configuration that
uses decimal points!)
Types are flyweights. This means there is only ever one instance of
a type and it is not allowed to contain any state. Creation of type
instances is abstracted through a static get method
``Doctrine\DBAL\Types\Type::getType()``.
.. note::
See the `Known Vendor Issue <./../known-vendor-issues>`_ section
for details about the different handling of microseconds and
timezones across all the different vendors.
.. warning::
All Date types assume that you are exclusively using the default timezone
set by `date_default_timezone_set() <http://docs.php.net/manual/en/function.date-default-timezone-set.php>`_
or by the php.ini configuration ``date.timezone``.
If you need specific timezone handling you have to handle this
in your domain, converting all the values back and forth from UTC.
Detection of Database Types
---------------------------
When calling table inspection methods on your connections
``SchemaManager`` instance the retrieved database column types are
translated into Doctrine mapping types. Translation is necessary to
allow database abstraction and metadata comparisons for example for
Migrations or the ORM SchemaTool.
Each database platform has a default mapping of database types to
Doctrine types. You can inspect this mapping for platform of your
choice looking at the
``AbstractPlatform::initializeDoctrineTypeMappings()``
implementation.
If you want to change how Doctrine maps a database type to a
``Doctrine\DBAL\Types\Type`` instance you can use the
``AbstractPlatform::registerDoctrineTypeMapping($dbType, $doctrineType)``
method to add new database types or overwrite existing ones.
.. note::
You can only map a database type to exactly one Doctrine type.
Database vendors that allow to define custom types like PostgreSql
can help to overcome this issue.
Custom Mapping Types
--------------------
Just redefining how database types are mapped to all the existing
Doctrine types is not at all that useful. You can define your own
Doctrine Mapping Types by extending ``Doctrine\DBAL\Types\Type``.
You are required to implement 4 different methods to get this
working.
See this example of how to implement a Money object in PostgreSQL.
For this we create the type in PostgreSQL as:
.. code-block:: sql
CREATE DOMAIN MyMoney AS DECIMAL(18,3);
Now we implement our ``Doctrine\DBAL\Types\Type`` instance:
::
<?php
namespace My\Project\Types;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
/**
* My custom datatype.
*/
class MoneyType extends Type
{
const MONEY = 'money'; // modify to match your type name
public function getSqlDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return 'MyMoney';
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
return new Money($value);
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
return $value->toDecimal();
}
public function getName()
{
return self::MONEY;
}
}
The job of Doctrine-DBAL is to transform your type into SQL declaration. You can modify the SQL declaration Doctrine will produce. At first, you must to enable this feature by overriding the canRequireSQLConversion method:
::
<?php
public function canRequireSQLConversion()
{
return true;
}
Then you override the methods convertToPhpValueSQL and convertToDatabaseValueSQL :
::
<?php
public function convertToPHPValueSQL($sqlExpr, $platform)
{
return 'MyMoneyFunction(\''.$sqlExpr.'\') ';
}
public function convertToDatabaseValueSQL($sqlExpr, AbstractPlatform $platform)
{
return 'MyFunction('.$sqlExpr.')';
}
Now we have to register this type with the Doctrine Type system and
hook it into the database platform:
::
<?php
Type::addType('money', 'My\Project\Types\MoneyType');
$conn->getDatabasePlatform()->registerDoctrineTypeMapping('MyMoney', 'money');
This would allow to use a money type in the ORM for example and
have Doctrine automatically convert it back and forth to the
database.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment