Showing posts with label sql. Show all posts
Showing posts with label sql. Show all posts

Tuesday, November 24, 2015

Symfony and Doctrine - One-to-Many Bidirectional associations

As you could saw in the two previous posts I am using 2 entities, Ads and SpecificItems, found in a One-to-Many relationship unidirectional. One Ad can have none or several SpecificItems.
What makes it unidirectional is the fact that the Many side(specific items) contain "ads" objects, but the "ads" do not know which are their specific items.

Example of specific items :  2012 (year of manufacturing  of a car etc. SpecificItems holds only the value, the type name (Year of manufacturing, Color etc) is hold in a different class (entity).

Because of the unidirectional relation at this moment I am retrieving the data with 2 different queries, first query get the ads, the second one retrieve the specific items which have ads in the first query result. Bot quires return data as arrays and not as object (as I selected only some fields).

$ads = $em->getRepository('CPANAClassifiedsBundle:Ads')
->findAllAdsMyPagination($currentPage, $limit);
        
 $allSpecificItems = $em->getRepository('CPANAClassifiedsBundle:SpecificItems')

->findItemsForAdsList($ads);

And in the view I am combining the two arrays.

I feel that I am not using really the Doctrine's capabilities by retrieving arrays  instead of actual objects. It is also more natural for an Ad to contain the list of specific items.



Official documentation Doctrine associations:
http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/association-mapping.html
http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/unitofwork-associations.html

1. First step - modify the entities

 In the One side of the One to Many, in the Ads entity, I've added $items, which is a collection object, it will hold all the associated SpecificItems objects.


use Doctrine\Common\Collections\ArrayCollection;
........

/**
     * @var integer
     *
     * @ORM\OneToMany(targetEntity="CPANA\ClassifiedsBundle\Entity\SpecificItems", mappedBy="idAds")
     * @ORM\JoinColumn(name="items_id", referencedColumnName="id")
     */

    private $items;

    public function __construct() {
        $this->items = new ArrayCollection();
    }

 
    public function getItems()
    {
        return $this->items;
    }
  
    public function addItem(\CPANA\ClassifiedsBundle\Entity\SpecificItems $item) {
        $this->items[] = $item;
        $item->setIdAds($this);
        return $this;
    }
  
    public function removeItems($item) {
        $this->items->removeElement($item);
    }


In the Many side (SpecificItems entity) I make sure to specific the inversedBy clause:

 /**
     * @var \CPANA\ClassifiedsBundle\Entity\Ads
     *
     * @ORM\ManyToOne(targetEntity="CPANA\ClassifiedsBundle\Entity\Ads", inversedBy="items")
     * @ORM\JoinColumns({
     *   @ORM\JoinColumn(name="id_ads", referencedColumnName="id_ads")
     * })
     */

    private $idAds;


  2. Modify the Repository

Make sure to add all the classes in the SELECT statement otherwise they will be loaded lazy resulting in a large number of  SELECTS into the database (N+1 queries issue in ORMs). I am using the pagination capabilities from Doctrine.
 
 public function findAllAdsCustom($currentPage = 1, $limit)
    {
        $query= $this->getEntityManager()
            ->createQuery(
                'SELECT  a,p,u,cat,c,ca,city,type
                FROM CPANAClassifiedsBundle:Ads a
                LEFT JOIN a.items p
                LEFT JOIN a.idUser u
                LEFT JOIN a.idCategory cat
                LEFT JOIN a.idCurrency c
                LEFT JOIN a.idCityArea ca
                LEFT JOIN ca.idCity city
                LEFT JOIN p.idSpecificItemType type
                ORDER BY a.idAds'
               
            );
           
        $paginator = $this->paginate($query, $currentPage, $limit);

        return $paginator;
    }
   
    public function paginate($dql, $page, $limit)
    {
        $paginator = new Paginator($dql);
        $paginator->setUseOutputWalkers(false);
          
        $paginator->getQuery()
            ->setFirstResult($limit * ($page - 1)) // Offset
            ->setMaxResults($limit); // Limit

        return $paginator;
    }

3. The controller

No need to call a special query for retrieving the SpecificItems, evrything will be now return in the all_ads array which will contain objects of the Ads class. And these Ads objects contain now the $items collection.
 
 public function showAllAction($currentPage)
    {
        $limit = 1;  // limit variable holds the number of ads per page
        $em = $this->getDoctrine()->getManager();
        $ads = $em->getRepository('CPANAClassifiedsBundle:Ads')->findAllAdsCustom($currentPage, $limit);
      
        $totalAds = $ads->count();
        $maxPages = ceil($totalAds/$limit);
        $thisPage = $currentPage;
      
      
        if (!$ads) {
            throw $this->createNotFoundException('Unable to find ads.');
        }
      
        return $this->render('CPANAClassifiedsBundle:Pub\Ads:show_all.html.twig', array(
            'all_ads' => $ads,
            'maxPages'=>$maxPages,
            'thisPage' => $thisPage,
        ) );
    }


4. The view

 {% extends 'CPANAClassifiedsBundle::layout.html.twig' %}

{% block leftcolumn %} {{ render(controller('CPANA\ClassifiedsBundle:Pub/Categories:index')) }} {% endblock %}

{% block content %}

<table style="width:100%">
    {% for ad in all_ads %}  
        <tr><hr></tr>
        <tr><h1> {{ ad.title }}</h1></tr>
        <tr><p><b>Posted by: </b> {{ ad.idUser.name }}</p></tr>
        <tr><p><b>Category: </b> {{ ad.idCategory.categoryName }}</p></tr>
        <tr><p><b>Area: </b> {{ ad.idCityArea.cityAreaName }}</p></tr>
        <tr><p><b>City: </b> {{ ad.idCityArea.idCity.cityName }}</p></tr>
        <tr><h2>Description</h2></tr>
        <tr><p>{{ ad.description }} </p></tr>
      
        {% for item in ad.items %}
            <tr><p><b> {{ item.idSpecificItemType.itemTypeName  }} </b> - {{ item.itemValue }} </p> </tr>
        {% endfor %}
      
        <tr><p><b>Price: </b> {{ ad.price }} -- <b> {{ ad.idCurrency.currencyName }} </b> </p></tr>
      
      
    {% endfor %}

</table>

    {% if maxPages > 1 %}
        <ul>
            {%if thisPage > 1 %}
                <li >
                    <a href="{{ path('cpana_classifieds_public_ads_show_all', {currentPage: thisPage-1 < 1 ? 1 : thisPage-1}) }}">«</a>
                </li>
            {% endif %}
     
            {# Render each page number #}
            {% for i in 1..maxPages %}
                <li>
                    <a href="{{ path('cpana_classifieds_public_ads_show_all', {currentPage: i}) }}">{{ i }}</a>
                </li>
            {% endfor %}

            {# `»` arrow #}
            {%if thisPage < maxPages %}
                <li>
                    <a href="{{ path('cpana_classifieds_public_ads_show_all', {currentPage: thisPage+1 <= maxPages ? thisPage+1 : thisPage}) }}">»</a>
                </li>
            {% endif %}
        </ul>
    {% endif %}

{% endblock %}


5. See the results

 When loading the "Show all ads" I can see them nicely paginated and also having the associated SpecificItems listed. In total 3 queries were executed to obtain the result, same as before when I had One-To-Many unidirectional relationship between ads and specific items. 




Sunday, November 1, 2015

Generate Entities from an Existing Database with Symfony and Doctrine

As said in a previous post, I am trying to see how easy is to create a Symfony bundle starting from an existing Database (like when you want to migrate an old app to Symfony). From Symfony documentation:

As the Doctrine tools documentation says, reverse engineering is a one-time process to get started on a project. Doctrine is able to convert approximately 70-80% of the necessary mapping information based on fields, indexes and foreign key constraints. Doctrine can't discover inverse associations, inheritance types, entities with foreign keys as primary keys or semantical operations on associations such as cascade or lifecycle events. Some additional work on the generated entities will be necessary afterwards to design each to fit your domain model specificities.

 Below the steps I made to generate entities from existing tables in database:

 1.  Change current path to home directory of my Symfony installation.
    Execute this command:

        php app/console doctrine:mapping:import --force CPANAClassifiedsBundle xml
    The metadata files are generated under "/src/CPANAClassifiedsBundle/Resources/config/doctrine/".   Delete .orm.xml files related to other tables than the ones related to your bundle

2.   Once the metadata files are generated, you can ask Doctrine to build related entity classes by executing   the following two commands.

        php app/console doctrine:mapping:convert annotation ./src/CPANA/ClassifiedsBundle

check if the data was exported into the correct path otherwise copy it to Entity directory.
  
        php app/console doctrine:generate:entities CPANAClassifiedsBundle


        NOTE:        If you want to have a one-to-many relationship, you will need to add it manually into the entity or to  the generated XML or YAML files. Add a section on the specific entities for one-to-many defining the   inversedBy and the mappedBy pieces.
        http://stackoverflow.com/questions/12493865/what-is-the-difference-between-inversedby-and-mappedby

3.  Delete the mapping generated by Doctrine in the directory "/src/CPANA/ClassifiedsBundle/Resources/config/doctrine".

You may need to clear the cache:
 php app/console doctrine:cache:clear-metadata
Also you need to modify the Ads entity to correctly point to your User entity. In my case:

    /**
     * @var \AppBundle\Entity\User
     *
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\User")
     * @ORM\JoinColumns({
     *   @ORM\JoinColumn(name="id_user", referencedColumnName="id")
     * })
     */
    private $idUser;


and
    public function setIdUser(\AppBundle\Entity\User $idUser = null)


Testing how the model classes (entities)  generated by Doctrine work 


1. Add a new route to routing.yml

    cpana_classifieds_categories_index:
        path:     /classifieds/categories
        defaults: { _controller: CPANAClassifiedsBundle:Categories:index }

   
2. Add controller class CategoriesController.php:

    class CategoriesController extends Controller
    {
        public function indexAction()
        {
            $em = $this->getDoctrine()->getEntityManager();
            $allCategories = $em->getRepository('CPANAClassifiedsBundle:Category')->findAll();
           
            if (!$allCategories) {
                throw $this->createNotFoundException('Unable to find categories.');
            }
           
            return $this->render('CPANAClassifiedsBundle:Categories:index.html.twig', array( 'all_cat' => $allCategories ) );
        }
    }


3. Add the index.html.twig view:

    {% extends 'CPANAClassifiedsBundle::layout.html.twig' %}

    {% block content %}
    <table style="width:100%">
        {% for category in all_cat %}
            <tr>
                <td>{{ category.getIdCategory() }}</td>
                <td>{{ category.getCategoryName() }}</td>
                {%  if category.getIdParent() != false  %}
                    <td>{{ category.getIdParent().getCategoryName() }}</td>
                {% else %}
                    <td>root category</td>
                {% endif %}
               
      </tr>
        {% endfor %}
    </table>
    {% endblock %}

--------------------------------------------------------------------------------------------------------------

Some explanations:

The foreign keys from my tables are transformed in objects holding the entire row for that foreign key id. Results from this that category.getIdParent() is returning an object. If you try to display that object you will get an error like "Catchable Fatal Error: "Object of class CPANA\ClassifiedsBundle\Entity\Category could not be converted to string"
So I will be displaying a certain property of that object, let's say CategoryName using the getter: "category.getIdParent().getCategoryName() ".

I had a problem because Doctrine transformed the column "parent_id" into "parent" field which is not good cause "parent" is a key word used by PHP. I had to alter the table and generate again the entities. I suppose Doctrine transformed "parent_id" to "parent" because of the default mapping logic:

5.11. Mapping Defaults

The @JoinColumn and @JoinTable definitions are usually optional and have sensible default values.
The defaults for a join column in a one-to-one/many-to-one association is as follows:

name: "<fieldname>_id"
referencedColumnName: "id"

As an example, consider this mapping:

    PHP

    <?php
    /** @OneToOne(targetEntity="Shipping") **/
    private $shipping;

    XML
    YAML

This is essentially the same as the following, more verbose, mapping:

    PHP

    <?php
    /**
     * @OneToOne(targetEntity="Shipping")
     * @JoinColumn(name="shipping_id", referencedColumnName="id")
     **/
    private $shipping;