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
 # ...


##through

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

Set the database to UTF8

After the Symfony project starts, a mistake experienced programmers will make is to forget to set their database default character set and collation (charset and collation), which eventually becomes latin type, which is the majority Database default. They may remember it the first time they operate it, but they completely forget it after typing two lines of related common commands in subsequent development:

$  php bin/console doctrine:database:drop --force
$  php bin/console doctrine:database:create

Setting UTF8 as the default character set of MySQL is as simple as configuring Just add a few lines of code to the file (usually the my.cnf file):

[mysqld]#Version 5.5.3 introduced "utf8mb4", which is recommendedcollation-server     = utf8mb4_general_ci # Replaces utf8_general_cicharacter-set-server = utf8mb4            # Replaces utf8

You can also change Doctrine's default character set so that the generated SQL uses the correct character set.

# app/config/config.ymldoctrine:
    dbal:
        charset: utf8mb4
        default_table_options:
            charset: utf8mb4
            collate: utf8mb4_unicode_ci
<!-- 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                charset="utf8mb4">
            <doctrine:default-table-option name="charset">utf8mb4</doctrine:default-table-option>
            <doctrine:default-table-option name="collate">utf8mb4_unicode_ci</doctrine:default-table-option>
        </doctrine:dbal>
    </doctrine:config></container>
// app/config/config.php$configuration->loadFromExtension('doctrine', array(
    'dbal' => array(
        'charset' => 'utf8mb4',
        'default_table_options' => array(
            'charset' => 'utf8mb4'
            'collate' => 'utf8mb4_unicode_ci'
        )
    ),));

We recommend avoiding using Mysql's uft8 character set because it is not compatible with 4-byte unicode characters and will be cleared if there are such characters in the string. However, this situation has been fixed, refer to New utf8mb4 character set.

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.

1466153595_56497_19601_doctrine_image_1.png

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 persist

Product 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/Product

This 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.

Remember, doctrine entity generator generates simple getters/setters. You should review the generated methods and, if necessary, add logic to meet the needs of your application.

More about doctrine:generate:entities

Using

doctrine:generate:entities Command you can:

  • Generate getters and setters in the entity class;

  • Configured in the entity class

    @ORM\ Entity(repositoryClass="...") Generate the corresponding repository class in the case of annotation;

  • Generate a suitable constructor for 1:n or n:m.

doctrine:generate:entities The command will save a backup of the original Product.php file and name it Product.php~. Sometimes this file may cause a "Cannot redeclare class" error. It can be safely removed. You can also use the –no-backup option to prevent these backup files from being generated.

Note that you do not

need (depend on) this command. You can also write getters and setters by hand. This option exists only to save you time since creating these methods is a common task during development.

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 the INSERT request to create a new row in the product 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.

1466160351_18163_67861_doctrine_web_debug_toolbar (1).png

#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:

  1. Get the object from Doctrine;
  2. Modify the object;
  3. 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.