Modeling with Doctrine
In this post, we try to use Doctrine to make the models richer, and make it more like a real world application.
Overview
Imagine there are several models in this application,
Album
, Artist
, Song
, Person
.
An
Artist
could compose many Album
s.
An
Album
could be accomplished by more than one Artist
.
An
Album
includes several Song
s.
An
Artist
is a generalized Person
.
In Doctrine ORM, it is easy to describe the relation between models.
Album
and Artist
is a ManyToMany relation.Album
and Song
is a OneToMany relation.Artist
is inherited from Person
.Codes of Models
The code of
Album
class./** * @ORM\Entity */ class Album { /** * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") * @ORM\Column(type="integer") */ private $id; /** @ORM\Column(type="string") */ private $title; /** * @ORM\ManyToMany(targetEntity="Artist", inversedBy="albums") * @ORM\JoinTable(name="albums_artists", * joinColumns={@ORM\JoinColumn(name="album_id", referencedColumnName="id")}, * inverseJoinColumns={@ORM\JoinColumn(name="artist_id", referencedColumnName="id")} * ) */ private $artists; /** * @ORM\OneToMany(targetEntity="Song", mappedBy="album", cascade="ALL", orphanRemoval=true, fetch="EXTRA_LAZY") */ private $songs; /** * @ORM\ElementCollection(tableName="tags") */ private $tags; public function __construct() { $this->songs = new ArrayCollection(); $this->artists = new ArrayCollection(); $this->tags = new ArrayCollection(); } ... }
Note, in the
__construct
method, songs and artists are initialized as ArrayCollection
. It is required by Doctrine ORM./** * @ORM\Entity */ class Song { /** * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") * @ORM\Column(type="integer") */ private $id; /** @ORM\Column(type="string") */ private $name; /** @ORM\Column(type="string") */ private $duration; /** * @ORM\ManyToOne(targetEntity="Album", inversedBy="songs") * @ORM\JoinColumn(name="album_id") */ private $album; }
Album
and Song
a bidirectional OneToMany relation./** * @ORM\Entity * @ORM\InheritanceType("JOINED") * @ORM\DiscriminatorColumn(name="person_type", type="string") * @ORM\DiscriminatorMap({"A"="Artist", "P"="Person"}) */ class Person { /** * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") * @ORM\Column(type="integer") */ private $id; /** @ORM\Column(type="string") */ private $name; public function getId() { return $this->id; } public function getName() { return $this->name; } public function setId($id) { $this->id = $id; } public function setName($name) { $this->name = $name; } } /** * @ORM\Entity */ class Artist extends Person{ /** * * @ORM\ManyToMany(targetEntity="Album", mappedBy="artists") */ private $albums; public function __construct() { $this->albums=new ArrayCollection(); } }
Artist
is derived from Person
, and inherits all features from Person
.
All the above codes, the setters and getters are omitted.
All the definition are very similar with JPA/Hibernate.
Doctrine supports two options of
InheritanceType
, SINGLE_TABLE and JOINED.
Generate the tables via doctrine command line tools.
vendor\bin\doctrine-module orm:schema-tool:create
if you are work on the database we used in before posts, use the following command instead.
vendor\bin\doctrine-module orm:schema-tool:update --force
This will synchronize the current schema with models.
Try to compare the generated tables when use SINGLE_TABLE and JOINED. The former only generate one table for
Artist
and Person
. The later generate two tables for Artist
and Person
, the common fields and the discriminator field are included in the person
table, when perform a query on Artist
, it will join the two tables(artist and person) by primary key, and return the result.Display details of an album
Now, create a details page to display the details of an album.
Ideally, a details page could include an cover image(use a dummy image here), album title, count of songs, artists, and the complete Song list.
By default, the relations of
Artist
and Song
are LAZY
. lazy is a very attractive feature when you access the related property which will be loaded on demand. But in RESTful architecture, the return result is generated by JSON/XML and sent to client. It is impossible access the unloaded relation as expected.
Update the
get
method of AlbumController
, we have to fetch the related properties together.public function get($id) { $em = $this ->getServiceLocator() ->get('Doctrine\ORM\EntityManager'); $album = $em->find('Album\Model\Album', $id); $results= $em->createQuery('select a, u, s from Album\Model\Album a join a.artists u join a.songs s where a.id=:id') ->setParameter("id", $id) ->getArrayResult(); //print_r($results); return new JsonModel($results[0]); }
Use a Doctrine specific fetch join to get album by id, and the related artists, songs in the same query.
Create a new album.html page.
<div ng-controller="AlbumCtrl"> <div class="row-fluid"> <div class="span3"> <img src="../../app/img/holder.png" width="128" height="128"> </div> <span class="span9"> <h2>{{album.title}}</h2> <p>{{album.songs.length}} SONGS, <span ng-repeat="u in album.artists">{{u.name}}</span></p> </span> </div> <table class="table"> <thead> <tr> <th>NAME</th> <th width="50px">DURATION</th> </tr> </thead> <tbody> <tr ng-repeat="e in album.songs"> <td>{{e.name}}</td> <td>{{e.duration}}</td> </tr> </tbody> </table> <p> <a href="#/albums" class="btn btn-success"> <b class="icon-home"></b>Back to Album List </a> </p> </div>
Add album routing and album controller.
//app.js $routeProvider ..... .when('/album/:id', {templateUrl: 'partials/album.html', controller: 'AlbumCtrl'})
//controllers.js as.controller('AlbumCtrl', function($scope, $rootScope, $http, $routeParams, $location) { $scope.album = {}; var load = function() { console.log('call load()...'); $http.get($rootScope.appUrl + '/albums/' + $routeParams['id']) .success(function(data, status, headers, config) { $scope.album = data; }); }; load(); });
Add some sample data, and run the project on Apache server.
Sample codes
Clone the sample codes from my github.com: https://github.com/hantsy/angularjs-zf2-sample
评论