Database and Doctrine ORM
One of the most common and challenging tasks for any application is to read and persist data information from the database. Although the symfony framework does not integrate any components that require the use of a database, it is tightly integrated with a third-party class library called Doctrine. Doctrine's main goal is to provide you with a powerful tool that makes database interaction easier and more flexible.
In this chapter, you will learn how to use doctrine to provide rich database interaction in Symfony projects.
Doctrine and symfony are completely decoupled, and their use is optional. This chapter is all about Doctrine ORM, which aims to let you map objects to relational databases (such as MySQL, PostgreSQL and Microsoft SQL). If you prefer to use the raw query of the database, this is very simple, you can refer to the article How to use Doctrine DBAL for explanation.
You can also use the Doctrine ODM class library to persist data to MongoDB. Refer to DoctrineMongoDBBundle for more information.
Simple example: a product ¶
To understand how Doctrine works, the easiest way is to look at an actual application. In this section, you need to configure your database, create a Product
object, persist it to the database, and then retrieve it.
Configuring the database ¶
Before you actually start, you need to configure your database connection information. By convention, this part of the information is usually configured in the app/config/parameters.yml
file:
# app/config/parameters.ymlparameters: database_host: localhost database_name: test_project database_user: root database_password: password # ...
parameters. yml to define the configuration is just a convention. When configuring Doctrine, parameters defined in that file will be referenced by the main configuration file:
# app/config/config.ymldoctrine: dbal: driver: pdo_mysql host: "%database_host%" dbname: "%database_name%" user: "%database_user%" password: "%database_password%"
<!-- app/config/config.xml --><?xml version="1.0" encoding="UTF-8" ?><container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:doctrine="http://symfony.com/schema/dic/doctrine" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd"> <doctrine:config> <doctrine:dbal driver="pdo_mysql" host="%database_host%" dbname="%database_name%" user="%database_user%" password="%database_password%" /> </doctrine:config></container>
// app/config/config.php$configuration->loadFromExtension('doctrine', array( 'dbal' => array( 'driver' => 'pdo_mysql', 'host' => '%database_host%', 'dbname' => '%database_name%', 'user' => '%database_user%', 'password' => '%database_password%', ),));By separating the database information into a separate file, you can easily save different versions for each server . You can also easily store database configuration (or any sensitive information) outside the project, just like the configuration information in Apache, for example. Refer to
How to set external parameters of service container to learn more.
Now Doctrine can connect to your database, the following command can automatically generate an empty test_project
database:
$ php bin/console doctrine:database:create
If you want to use SQLite as the database, set your database path in the path option:
# app/config/config.ymldoctrine: dbal: driver: pdo_sqlite path: "%kernel.root_dir%/sqlite.db" charset: UTF8
<!-- app/config/config.xml --><?xml version="1.0" encoding="UTF-8" ?><container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:doctrine="http://symfony.com/schema/dic/doctrine" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd"> <doctrine:config> <doctrine:dbal driver="pdo_sqlite" path="%kernel.root_dir%/sqlite.db" charset="UTF-8" /> </doctrine:config></container>
// app/config/config.php$container->loadFromExtension('doctrine', array( 'dbal' => array( 'driver' => 'pdo_sqlite', 'path' => '%kernel.root_dir%/sqlite.db', 'charset' => 'UTF-8', ),));
Create an Entity class ¶
Suppose you are building a program where some products need to be displayed. Even without thinking about Doctrine or the database, you already know that you need a Product
object to represent these products. Create this class in the Entity
directory of your AppBundle:
// src/AppBundle/Entity/Product.phpnamespace AppBundle\Entity; class Product{ private $name; private $price; private $description;}
This class - often called an "Entity", represents a basic class that holds data - It is simple and meets the business needs of the products required in the program. This class cannot be saved to the database yet - it is just a simple PHP class.
Once you learn the concepts behind Doctrine, you can let Doctrine create entity classes for you. It will ask you some interactive questions to help you create any entity:
$ php bin/console doctrine:generate:entity
Add mapping information ¶
Doctrine allows you to use the database in a more interesting way, instead of just taking the rows of scalar data into an array. Doctrine allows you to retrieve the entire object from the database and persist the entire object to the database at the same time. For Doctrine to achieve this, you must map the data table to a specific PHP class, and the columns of those tables must be mapped to specific attributes of the corresponding PHP class.
You need to provide this mapping information in the form of "meatdata". There is a set of rules that accurately tell Doctrine the Product
class and its How properties should be mapped to a specific data table. This metadata can be specified in different formats, including YAML, XML or directly defined into the Product
class through DocBlock annotations (Annotation: annotations):
// src/AppBundle/Entity/Product.phpnamespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="product") */class Product{ /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(type="string", length=100) */ private $name; /** * @ORM\Column(type="decimal", scale=2) */ private $price; /** * @ORM\Column(type="text") */ private $description;
# src/AppBundle/Resources/config/doctrine/Product.orm.ymlAppBundle\Entity\Product: type: entity table: product id: id: type: integer generator: { strategy: AUTO } fields: name: type: string length: 100 price: type: decimal scale: 2 description: type: text
<!-- src/AppBundle/Resources/config/doctrine/Product.orm.xml --><?xml version="1.0" encoding="UTF-8" ?><doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd"> <entity name="AppBundle\Entity\Product" table="product"> <id name="id" type="integer"> <generator strategy="AUTO" /> </id> <field name="name" type="string" length="100" /> <field name="price" type="decimal" scale="2" /> <field name="description" type="text" /> </entity></doctrine-mapping>
a The bundle can only accept one metadata definition format. For example, you cannot mix YAML metadata definitions and PHP entity classes with annotations added.
#The table name is optional, if omitted, it will automatically depend on the name of the entity class.
Doctrine allows you to choose from a wide range of field types, each with its own configuration. For information on available field types, see Doctrine Field Type Reference.
You can also view the Doctrine official document Basic Mapping Documentation to learn all the details about mapping. If you use annotations, you need to add ORM\
(for example ORM\Column(...)
) to all annotations, which is not stated in the Doctrine documentation. You also need to include the use Doctrine\ORM\Mapping as ORM;
declaration, which can import (import) ORM
annotation prefix.
Be careful that the Entity class name (or its properties) is also a SQL reserved keyword (such as group
and user
). For example, if your entity class name is Group
, then by default, your table name will be group, which may cause SQL errors in some database engines. Refer to the Reserved SQL keywords documentation to learn how to correctly circumvent these names. Optionally, you can arbitrarily choose the schema of the database and easily map it to different table names or column names. See the Creating Classes for the Database and Property Mapping documentation.
When using other libraries or programs that use annotations (such as Doxygen), you should add the @IgnoreAnnotation
annotation to the class to indicate that Symfony should Which annotation is ignored.
For example, to avoid the @fn
annotation from throwing an exception, add the following annotation:
/** * @IgnoreAnnotation("fn") */class Product// ...
After creating the entity, You should use the following command to verify mappings:
$ php bin/console doctrine:schema:validate
Generate Getters and Setters ¶
##Although Doctrine now knows how to persistProduct Object to database, but the class itself has no real purpose yet. Since
Product is just a regular PHP class with
private properties, you need to create
public getter and setter methods (such as
getName(),
setName($name)) in order to access its properties (its properties are protected) in other parts of the program. Fortunately, the following command can automatically generate these templated methods:
$ php bin/console doctrine:generate:entities AppBundle/Entity/ProductThis command ensures that all getters and setters of the
Product class are generated. This is a safe command line - you can run it multiple times and it will only generate getters and setters that don't exist (i.e. it won't replace existing methods).
Important TipsThe following sentence is extremely profound and is the key to using Doctrine effectively. Everyone must do it.
You can also generate getters and setters for all known entities in a bundle or an entity namespace (any PHP class containing Doctrine mapping information):
# generates all entities in the AppBundle# 生成AppBundle下的全部entities$ php bin/console doctrine:generate:entities AppBundle # generates all entities of bundles in the Acme namespace # 生成Acme命名空间下的bundles的全部entities $ php bin/console doctrine:generate:entities Acme
Create data table/Schema ¶
Now you have a working Product
class that contains the mapping information, so Doctrine knows exactly how to persist it. Of course, you don't have the corresponding product
data table in the library yet. Fortunately, Doctrine can create all data tables automatically. To do this, run the following command:
$ php bin/console doctrine:schema:update --force
Seriously, this command is surprisingly powerful. It compares what your database should theoretically look like (based on your entities' mapping information) with actually what it should look like, and then executes the required SQl statements To update the database schema to what it should look like. In other words, if you add a new property containing "mapping metadata" to Product
and run this task, it will execute the required "ALTER TABLE" statement to the existing Add that new column to the product
table.
A better way to take advantage of this feature is through migrations, which allows you to generate these SQL statements and store them into migration classes, which can be run in order Safely and reliably update and track database schema changes in your production environment.
Regardless of whether you utilize database migration, the doctrine:schema:update
command is only suitable for use in a development environment. It should not be used in production environments.
You now have a fully functional product table in your database with columns that match the metadata you specified.
Persistent object to database ¶
Now you have a Product entity and the product database table mapped to it. You can persist data into a database. Inside a Controller it's very simple. Add the following method to the bundle's DefaultController.
Now that you have mapped the Product
entity to the corresponding product
table, you are ready to persist the Product
object to in the database. Inside the controller, this is extremely simple. Add the following method to bundle's DefaultController
:
// src/AppBundle/Controller/DefaultController.php // ...use AppBundle\Entity\Product;use Symfony\Component\HttpFoundation\Response; // ...public function createAction(){ $product = new Product(); $product->setName('Keyboard'); $product->setPrice(19.99); $product->setDescription('Ergonomic and stylish!'); $em = $this->getDoctrine()->getManager(); // tells Doctrine you want to (eventually) save the Product (no queries yet) // 告诉Doctrine你希望(最终)存储Product对象(还没有语句执行) $em->persist($product); // actually executes the queries (i.e. the INSERT query) // 真正执行语句(如,INSERT 查询) $em->flush(); return new Response('Saved new product with id '.$product->getId());}
If you are following this routine, you need to create a route and point it to this action to see it run.
This example shows the use of Doctrine's getDoctrine() method in the controller. This is a quick way to take out the doctrine
service. If you inject this service into your service, you can use doctrine anywhere. Refer to Service Container to learn more about creating services.
A closer look at the previous example:
- Lines 10-13 is instantiated here and behaves like any other regular PHP object Use the
$product
object the same way. - Line 15 This line takes out the entity manager object of Doctrine, which is responsible for handling the persistence (Annotation: writing) of the database and the process of taking out the object.
- Line 18
persist($product)
is called to tell Doctrine to "manage" the$product
object. It didn't trigger a request to the database. - Line 21 When the
flush()
method is called, Doctrine will traverse all the objects it manages to determine whether they need to be persisted to the database. In this example, the data of the$product
object does not exist in the database, so the entity manager needs to execute theINSERT
request to create a new row in theproduct
table .
In fact, since Doctrine knows all of your managed entities, it will calculate all changes when you call the flush()
method Set (changeset) and execute statements in the correct order. It utilizes prepared cached statements to slightly improve performance. For example, if you want to persist a total of 100 Product
objects, and then call the flush()
method, Doctrine will use a single prepare syntax object to execute 100 INSERT
ask.
If the flush()
call fails, a Doctrine\ORM\ORMException
exception will be thrown. See Transactions and Concurrency.
The workflow is the same when creating and updating objects. In the next section you'll see how Doctrine cleverly automatically issues an Update
statement if the record already exists in the database.
Doctrine provides a class library that allows you to programmatically load test data into your project (ie, "fixture data", fixed data). Refer to DoctrineFixturesBundle to learn more.
Getting objects from the database ¶
Getting objects from the database is even simpler. For example, if you configure a Routing, based on the product's id
to display a specific Product
object:
public function showAction($productId){ $product = $this->getDoctrine() ->getRepository('AppBundle:Product') ->find($productId); if (!$product) { throw $this->createNotFoundException( 'No product found for id '.$productId ); } // ... do something, like pass the $product object into a template // ... 做一些事,比如把 $product 对象传入模板}
You can use the @ParamConverter
shortcut annotation, no need Write any code to achieve the same functionality. Refer to FrameworkExtraBundle to learn more.
When you want to query a specific type of object, you always use its "respository". You can think of a Respository as a PHP class whose only job is to help you get entities out of that particular class. For an entity class, to access its treasure house, pass:
$repository = $this->getDoctrine() ->getRepository('AppBundle:Product');
appBundle:Product
is a shortcut method that you can use anywhere in Doctrine to replace the FQCN class of the entity class name (such as AppBundle\Entity\Product
). As long as your entity is stored in the bundle's Entity
namespace, it will work.
Once you have a Repository object, you can access all of its useful methods.
$repository = $this->getDoctrine()->getRepository('AppBundle:Product'); // query for a single product by its primary key (usually "id")// 通过主键(通常是id)查询一件产品 $product = $repository->find($productId); // dynamic method names to find a single product based on a column value// 动态方法名称,基于字段的值来找到一件产品$product = $repository->findOneById($productId);$product = $repository->findOneByName('Keyboard'); // dynamic method names to find a group of products based on a column value // 动态方法名称,基于字段值来找出一组产品$products = $repository->findByPrice(19.99); // find *all* products / 查出 *全部* 产品$products = $repository->findAll();
Of course, you can also use complex queries, refer to the Object Query section.
You can also make effective use of the findBy
and findOneBy
methods to easily obtain objects based on multiple conditions:
$repository = $this->getDoctrine()->getRepository('AppBundle:Product'); // query for a single product matching the given name and price// 查询一件产品,要匹配给定的名称和价格$product = $repository->findOneBy( array('name' => 'Keyboard', 'price' => 19.99)); // query for multiple products matching the given name, ordered by price// 查询多件产品,要匹配给定的名称和价格$products = $repository->findBy( array('name' => 'Keyboard'), array('price' => 'ASC'));
When rendering any page, you can see a number of queries in the lower right corner of the web debug toolbar.
#If you click on the icon, the profiler will open showing the exact query that was generated.
If your page queries exceed 50, the icon will turn yellow. This indicates that something is not quite right.
Object Update ¶
Once you have obtained an object from Doctrine, updating it is easy. Suppose you have a route that maps a product id to the controller's update action:
public function updateAction($productId){ $em = $this->getDoctrine()->getManager(); $product = $em->getRepository('AppBundle:Product')->find($productId); if (!$product) { throw $this->createNotFoundException( 'No product found for id '.$productId ); } $product->setName('New product name!'); $em->flush(); return $this->redirectToRoute('homepage');}
Updating an object involves three steps:
- Get the object from Doctrine;
- Modify the object;
- Call the
flush()
method of the entity manager.
Note that calling $em->persist($product)
is unnecessary. Recall that this method just tells Doctrine to manage or "observe" the $product
object. Here, because you have already obtained the $product
object, it has already been managed.
Deleting an object ¶
Deleting an object is very similar, but you need to call remove()
from the entity manager. Method:
$em->remove($product);$em->flush();
As you might have expected, the remove()
method notifies Doctrine that you want to remove the specified entity from the database. The real DELETE
query will not be actually executed until the flush()
method is called.
Object Query ¶
You have seen how the repository object allows you to perform some basic queries without having to do any work:
$repository = $this->getDoctrine()->getRepository('AppBundle:Product'); $product = $repository->find($productId);$product = $repository->findOneByName('Keyboard');
Of course, Doctrine also allows you to use Doctrine Query Language (DQL) to write some complex queries. DQL is similar to SQL, except that it is used to query one or more entity class objects (such as product
) , while SQL queries rows in a data table (such as product
).
When querying in Doctrine, you have two main options: write pure Doctrine queries (DQL) or use Doctrine's Query Builder.
Using DQL for object query ¶
Suppose you want to query products with a price higher than 19.99
, and sort them from low to high. You can use DQL, a syntax similar to native SQL in Doctrine, to construct a query for this scenario:
$em = $this->getDoctrine()->getManager();$query = $em->createQuery( 'SELECT p FROM AppBundle:Product p WHERE p.price > :price ORDER BY p.price ASC')->setParameter('price', 19.99); $products = $query->getResult();
If you are used to writing SQL, then DQL will also be very natural. The biggest difference between them is that you need to think in terms of "select PHP objects", not rows of a data table. Because of this, you start with AppBundle:Product
the entity (optional shortcut for a AppBundle\Entity\Product
class ) to select, and then give the entity an alias of p
.
Note the setParameter()
method. When using Doctrine, it is a good idea to set arbitrary external values via "placeholders" (:price
in the example above) as it prevents SQL injection attacks.
getResult()
The method returns a result array. To get a result, you can use getSingleResult()
(this method will throw an exception when there is no result) or getOneOrNullResult()
:
$product = $query->setMaxResults(1)->getOneOrNullResult();
DQL syntax is powerful Incredibly, it allows easy joins between entities (relations will be covered later), groups, etc. Refer to the Doctrine Query Language documentation to learn more.
Use Doctrine's Query Builder for object query ¶
Instead of writing a large string in DQL, you can use a very useful QueryBuilder object to build that string . This is useful when your query depends on dynamic conditions, because as your connection string keeps growing, the DQL code becomes increasingly difficult to read:
$repository = $this->getDoctrine() ->getRepository('AppBundle:Product'); // createQueryBuilder() automatically selects FROM AppBundle:Product// and aliases it to "p"// createQueryBuilder() 自动从 AppBundle:Product 进行 select 并赋予 p 假名$query = $repository->createQueryBuilder('p') ->where('p.price > :price') ->setParameter('price', '19.99') ->orderBy('p.price', 'ASC') ->getQuery(); $products = $query->getResult();// to get just one result: / 要得到一个结果:// $product = $query->setMaxResults(1)->getOneOrNullResult();
The QueryBuilder object contains all the Necessary method. By calling the getQuery() method, the query builder will return a standard Query object that can be used to obtain the requested result set.
Query BuilderFor more information, refer to Doctrine's Query Builder documentation.
Organize custom queries into the Repository class ¶
All the previous queries are written directly in your controller. But for program organization, Doctrine provides a dedicated repository class that allows you to save all query logic to a central location.
Reference How to Create a Custom Repository Class to learn more.
Configuration ¶
Doctrine is highly configurable, although you may never care about those options. For Doctrine configuration information, see the config reference.
Doctrine Field Type Reference ¶
Doctrine comes with a large number of available field types. Each can map PHP data types to specific field types, no matter what database you use. For each field type, Column
can be further configured to set length
, nullable
behavior, name
or other options. For a list of available field types, see the Mapping Types documentation.
Associations and Relations ¶
Doctrine provides all the functionality you need to manage database relationships (also known as associations). For more information, see How to use Doctrine Associations/Relations.
Summary ¶
With Doctrine, you can focus on your object and how to apply it to the program, while database persistence comes second. This is because Doctrine allows you to use any PHP object to save your data, and relies on "metadata mapping" information to map an object's data to a specific data table.
Doctrine has many powerful features waiting for you to learn, such as relationships, complex queries and event monitoring.