How to use a data mapper with sql queries

2019-08-05 04:33发布

问题:

I am having trouble understanding the datamapper design pattern. I have two queries (one to get albums and one to get artist). I want to produce a list of albums and artists (band members). There is a one to many relationship between the two.

SQL Queries

public function getArtist()
{
    $adapter = $this->getAdapter();

    $sql = new Sql($adapter);
    $select = $sql->select();
    $select->from('genre');  
    $select->where(array(
            'album_id' => $album,));
    $selectString = $sql->getSqlStringForSqlObject($select);
    $resultSet = $adapter->query($selectString, $adapter::QUERY_MODE_EXECUTE);
    return $resultSet;  
}  

public function getAlbum()
{
    $adapter = $this->getAdapter();

    $sql = new Sql($adapter);
    $select = $sql->select();
    $select->from('album');  
    $selectString = $sql->getSqlStringForSqlObject($select);
    $resultSet = $adapter->query($selectString, $adapter::QUERY_MODE_EXECUTE);
    return $resultSet;  
}  

When I do something similar to below I end up running a separate query for each album. I don't want to do this. How do I go about setting something like this up http://akrabat.com/php/objects-in-the-model-layer-part-2/ ? Can I set up so each entity represents a query (I would like to use joins in my queries as opposed to creating an entity for each table)?

 <?php foreach ($albums->getAlbum() as $album) : ?>
 <tr>
     <td><?php echo $this->escapeHtml($album->id);?></td>
     <td><?php echo $this->escapeHtml($album->title);?></td>
     <td><?php echo $this->escapeHtml($album->artist);?></td>
     <td>
           <?php foreach ($albums->getArtist($album->id) as $member) : ?>

Using Solution Provided Below

ConcreteAlbumMapper.php

use Zend\Db\Adapter\AdapterInterface;
use Album\Model\AlbumMapper;

namespace Album\Model;

class ConcreteAlbumMapper implements AlbumMapper {


    public function __construct(AdapterInterface $dbAdapter)
     {
        $this->adapter =$dbAdapter;
     }

    public function fetchAlbums(ArtistMapper $artistMapper) {
           $adapter = $this->getAdapter();
            $sql = new Sql($adapter);
            $select = $sql->select();
            $select->from('album');  
            $selectString = $sql->getSqlStringForSqlObject($select);
            $albumRows = $adapter->query($selectString, $adapter::QUERY_MODE_EXECUTE);

        $albums = array();

        foreach ($albumRows as $albumRow) {
            $albums[$albumRow['id']] = new Album($albumRow);
        }

        $artists = $artistMapper->fetchByAlbumIds(array_keys($albums));

        //marrying album and artists
        foreach ($artists as $artist) {
            /**
             * not crazy about the getAlbumId() part, would be better to say
             * $artist->attachYourselfToAnAlbumFromThisIndexedCollection($albums);
             * but going with this for simplicity
             */
            $albums[$artist->getAlbumId()]->addArtist($artist);
        }

        return $albums;
    }

}

ConcreteMemberMapper.php

<?php

namespace Album\Model;

use Zend\Db\Adapter\AdapterInterface;
use Zend\Db\Sql\Sql;
use Album\Model\Member;

class ConcreteMemberMapper { 



   public function __construct(AdapterInterface $dbAdapter)
     {
        $this->adapter =$dbAdapter;
     } 

     public function getAdapter()
    {
   if (!$this->adapter) {
      $sm = $this->getServiceLocator();
      $this->adapter = $sm->get('Zend\Db\Adapter\Adapter');
    }
    return $this->adapter;
    }

    public function fetchByAlbumIds($ids) {
        $adapter = $this->getAdapter();

        $sql = new Sql($adapter);
        $select = $sql->select();
        $select->from('members');
        $select->where(array(
                'album_id' => $ids));
        $selectString = $sql->getSqlStringForSqlObject($select);
        $results = $adapter->query($selectString, $adapter::QUERY_MODE_EXECUTE);
        return $results;
        //return \Zend\Stdlib\ArrayUtils::iteratorToArray($results);
    }

    public function getAlbumId()
    {
        return $this->albumId;
    }

}

Album.php

<?php
namespace Album\Model;

 use Zend\InputFilter\InputFilter;
 use Zend\InputFilter\InputFilterAwareInterface;
 use Zend\InputFilter\InputFilterInterface;

 class Album //implements InputFilterAwareInterface
 {

    public $id;
    public $title;
    public $artist;
    public $members = array();

    public function __construct($data) {
         $this->id     = (!empty($data['id'])) ? $data['id'] : null;
         $this->artist = (!empty($data['artist'])) ? $data['artist'] : null;
         $this->title  = (!empty($data['title'])) ? $data['title'] : null;
         $this->members  = (!empty($data['members'])) ? $data['members'] : null;
    }

    public function addMember(Member $member) {
        $this->members[] = $member;
    }

AlbumController.php

 public function indexAction()
 {
     $dbAdapter = $this->getServiceLocator()->get('Zend\Db\Adapter\Adapter');  
     //$album = new Album(); 
     $albumMapper = new ConcreteAlbumMapper($dbAdapter);
     $memberMapper = new ConcreteMemberMapper($dbAdapter);
     $bar=$albumMapper->fetchAlbums($memberMapper);
     //$artistMapper = new 
     //var_dump($bar);
    return new ViewModel(array(
        'albums' => $bar,
     ));
 }

Many Thanks
Matt

回答1:

There are many ways to approach this, this is one of them. Let's say you have a db schema similar to this:

Now you can have AlbumMapper and ArtistMapper responsible for fetching those objects from the db for you:

interface AlbumMapper {

    /**
     * Fetches the albums from the db based on criteria
     * @param type $albumCriteria
     * @param ArtistMapper $artistMapper
     * 
     * Note: the ArtistMapper here can be also made optional depends on your app
     */
    public function fetchBySomeCriteria($albumCriteria, ArtistMapper $artistMapper);
}

interface ArtistMapper {

    /**
     * @param array $ids
     * @return Artist[]
     */
    public function fetchByAlbumIds(array $ids);
}

I've put that AlbumMapper requires ArtistMapper so with this mapper Albums are always returned with their artists. Now an example implementation can be like this where I use a little indexing trick to attach artist to albums:

class ConcreteAlbumMapper implements AlbumMapper {

    public function fetchBySomeCriteria($albumCriteria, ArtistMapper $artistMapper) {
        //sql for fetching rows from album table based on album criteria

        $albums = array();

        foreach ($albumRows as $albumRow) {
            $albums[$albumRow['id']] = new Album($albumRow);
        }

        $artists = $artistMapper->fetchByAlbumIds(array_keys($albums));

        //marrying album and artists
        foreach ($artists as $artist) {
            /**
             * not crazy about the getAlbumId() part, would be better to say
             * $artist->attachYourselfToAnAlbumFromThisIndexedCollection($albums);
             * but going with this for simplicity
             */
            $albums[$artist->getAlbumId()]->addArtist($artist);
        }

        return $albums;
    }

}

In this case your Album will be along the lines of:

class Album {

    private $id;
    private $title;
    private $artists = array();

    public function __construct($data) {
        //initialize fields
    }

    public function addArtist(Artist $artist) {
        $this->artists[] = $artist;
    }

}

At the end of all this you should have a collection of Album objects initialized with their Artists.