In REST how do you retrieve resources identified by 2 attributes? - web-services

In REST, if you want to retrieve a resource, you use something like:
http://www.blabla.com/api/movies/1234
where 1234 is the unique identifier of the movie in the database.
The question I have how do you retrieve entities from the database that are identified by 2 attributes?
Maybe something like:
http://www.blabla.com/api/grade/1234/764334532 (identified by the combination of user and exam id)
Or perhaps I have to model the database or resources in other way, like adding an id to grades in the database.
What do you think? Thanks.

If your exam id is globally unique you can just use that in the URI. no need to reference the user:
http://www.blabla.com/api/exams/123
if the exam id is only locally unique to a user, then you need to include both, as follows:
http://www.blabla.com/api/users/456/exams/123

You need to take care that your ressources are addressed uniquely. Below there is an example (JAX-RS) of a customer service which enables users to insert and retrieve recensions to or from a specific product.
/**
* Abstract service customer. Contains methods for retrieving and persisting
* data in database.
*/
#Path("/customers")
public interface ServiceCustomer extends Service<Customer, Integer> {
/**
* Adds a customers {#link Recension} to a product
*
* #param recension
* The new created {#link Recension} by the customer
* #param customerId
* Identifier of the customer as Integer who has created the
* recension
* #param productId
* Identifier of the product as Integer which should be
* associated with the new created recension
* #return {#link Response}
*/
#POST
#Path("{id}/productId/{prodId}/recension")
#Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response insertRecension(Recension recension,
#PathParam("id") Integer customerId,
#PathParam("prodId") Integer productId);
/**
* Retrieves all customers {#link Recension}(s)
*
* #param customerId
* Identifier of the {#link Customer} as int
* #return {#link Response}, containing a {#link List} of all created
* {#link Recension}(s) by the customer
*/
#GET
#Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
#Path("{id}/recension")
public Response getRecensions(#PathParam("id") int customerId);
/**
* Retrieves a single {#link Recension} of a {#link Product} which was
* created by a specific {#link Customer}
*
* #param productID
* Identifier of the {#link Product}
* #param customerID
* Identifier of the {#link Customer}
* #return {#link Response}, which contains the {#link Recension} of the
* {#link Product} created by the user. Assuming the user has
* created a recension.
*/
#GET
#Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
#Path("{id}/productId/{prodId}/recension")
public Response getRecensionOfProductByCustomer(
#PathParam("prodId") int productID, #PathParam("id") int customerID);
}
The first method insertRecension is used by a customer to create a recension to a specific product. Hereby you need to define two identifiers, the id of the customer who is creating the recension for the product with the specific id.
#Path("{id}/productId/{prodId}/recension")
The second method getRecensions is used by a customer to retrieve all his created recensions. For this purpose, the rest- endpoint just needs to know a single identifier, the customers id.
#Path("{id}/recension")
The last method getRecensionOfProductByCustomer retrieves the recension of a specific product, created by a specific user. Notice, that this rest- endpoint takes two identifiers too!
#Path("{id}/productId/{prodId}/recension")

It's hard to say without more information, but I'd consider a lightweight /grades URI, where each grade is the intersection of an exam and a user:
GET /grades?exam=3&userId=bob
{
"id": 343,
"self": "/grades/343",
"exam": "/exams/3",
"user": "/users/bob",
"value": 88
}
You can then easily find all the grades for an exam or a user by only specifying one of the two query parameters.

Related

Doctrine - Prevent double flush on using identity through foreign entity

I've got 2 entities - TestUser and TestAddress. Address has OneToOne relation with User and it's primary key is also foreign key to User.
/**
* #ORM\Entity
*/
class TestUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
private $id;
}
/**
* #ORM\Entity
*/
class TestAddress
{
/**
* #ORM\Id
* #ORM\OneToOne(targetEntity="TestUser")
*/
private $user;
/**
* #param $user
*/
public function setUser($user)
{
$this->user = $user;
}
}
When I try to save both entities through entityManager I get an Exception.
$em = $this->getEntityManager();
$user = new TestUser();
$address = new TestAddress();
$address->setUser($user);
$em->persist($user);
$em->persist($address);
$em->flush();
Exception:
Doctrine\ORM\ORMInvalidArgumentException: The given entity of type 'Entity\TestAddress' (Entity\TestAddress#000000004fcda1bf000000002f07b49b) has no identity/no id values set. It cannot be added to the identity map.
Only way to save both entities is "flush" with User and then persist and flush with address.
$em->persist($user);
$em->flush();
$em->persist($address);
$em->flush();
Question: Is there way to use autoincrement on TestUser id and save TestUser and TestAddress entities with one flush?
class TestAddress
{
/**
* #ORM\OneToOne(targetEntity="TestUser", cascade={"persist"})
*/
private $user;
I know it is not even an answer to your question, but IMO you should rethink the purpose of this schema choice when the ID of the Address entity must correspond to the ID of the User entity. I do not think this is necessary and you would be better off by going with separate foreign key and primary key fields on Address entity.
However one other thing you could try is making the relationship bi-directional, that is defining the testAddress variable on the User object and then adding the corresponding mapping information to it:
/**
* #OneToOne(targetEntity="TestAddress", mappedBy="user", cascade={"persist"})
*/
private $testAddress;
then you will probably also need to create the setTestAddress() method on the user entity, and set the user object for TestAddress explicitly in there:
public function setTestAddress($address)
{
$address->setUser($this);
$this->testAddress = $address;
}
then you could call $user->setTestAddress($address) and try flushing only the user entity:
$user = new TestUser();
$address = new TestAddress();
$user->setTestAddress($address);
$em->persist($user);
$em->flush();
I am not quite familiar with the order in which doctrine persists entities, but by having the entity be persisted by the TestUser entity and not the other way around(as was suggested) User should be persisted first and than its ID added to the Address before it is even persisted.
I didn't test the code so there might be some errors in it.

Entity-class mapping invalid: inconsistent with each other

I have a table "cms_objects" // Object.php - that stores all object info
I have another table "cms_media" // Media.php - that stores all media info
An object can have many media items (post with lots of different images)
In Object.php
/**
* #ORM\OneToMany(targetEntity="Media", mappedBy="Object")
*/
private $cms_media;
In Media.php
/**
* #ORM\ManyToOne(targetEntity="Object", inversedBy="cms_media")
* #ORM\JoinColumn(name="object_id", referencedColumnName="id")
*
* #Annotation\Exclude()
*/
private $object;
When I run: php public/index.php orm:validate-schema - I get:
[Mapping] FAIL - The entity-class 'Application\Entity\Cms\Media' mapping is invalid:
* The mappings Application\Entity\Cms\Media#object and Application\Entity\Cms\Object#cms_media are inconsistent with each other.
[Mapping] FAIL - The entity-class 'Application\Entity\Cms\Object' mapping is invalid:
* The association Application\Entity\Cms\Object#cms_media refers to the owning side field Application\Entity\Cms\Media#Object which does not exist.
Ideally, I need to be able to create a ZF2 form with element: 'media' or 'cms_media' but I haven't been able to validate it yet...
You can try to use FQCN inside the annotations. Instead of
/**
* #ORM\OneToMany(targetEntity="Media", mappedBy="Object")
*/
try
/**
* #ORM\OneToMany(targetEntity="Application\Entity\Cms\Media", mappedBy="Object")
*/
in both entities.
Also i would like to recommend using camelCased entity properties instead of underscored_ones. Hydration process of the entities with underscored properties using DoctrineObject hydrator is problematic. You can find more details here.
BEWARE - Using unnecessary bi-directional associations increases your object graph and domain model complexity. Best practice is avoiding bi-directional associations if possible.
For this case, you can rewrite the same mapping using uni-directional relation between Post (Object) and Media entities if you don't need reverse access from Media to Post like
$media->getPost()
For example Application/Entity/Cms/Post.php :
/** #ORM\Entity **/
class Post
{
/**
* One to many, unidirectional
* #ORM\ManyToMany(targetEntity="Application\Entity\Cms\Media")
* #ORM\JoinTable(name="post_to_media",
* joinColumns={#ORM\JoinColumn(name="post_id", referencedColumnName="id")},
* inverseJoinColumns={
* #ORM\JoinColumn(name="media_id", referencedColumnName="id",unique=true)
* })
**/
private $media;
public function __construct()
{
$this->media = new \Doctrine\Common\Collections\ArrayCollection();
}
// ...
}
and Application/Entity/Cms/Media.php :
/** #ORM\Entity **/
class Media
{
// No need to know about post
}

Manually add new column in Doctrine2 not working

I inherited a zend framework code with doctrine2.
I am trying to add a new column manually to no avail.
1) I create the last_viewed column in db which is dateTime format.
2) I added in the entity file
/**
* #var datetime $lastViewed
*/
protected $last_viewed;
/**
* Set lastViewed
*
* #param datetime $lastViewed
*/
public function setlastViewed($lastViewed) {
$this->last_viewed = $last_viewed;
}
/**
* Get lastViewed
*
* #return datetime
*/
public function gelastViewed() {
return $this->last_viewed;
}
3) Updated the YML file
last_viewed:
type: datetime
But when I try and retrive via
$user->gelastViewed()
I get an empty value.
Also when the entity proxy is generated I don't see the column name in the function "__sleep" rest all other columns are there.
Any suggestions as to how to add this manually.
Turns out cache was the culprit. Restarted Memcache and it works now !!!

Delete operations in doctrine 2

For eg I have entities like User,Item, Image. User has many items. Item has many images.
Which delete option should I choose, cascade={'remove'} or onDelete=Cascade ?
Also I have life cycle callbacks on Image. I know the difference between above mentioned cascade options. I was wondering if I used onDelete=cascade option, on deleting a User object, will the life cycle callback like PostRemove() be called ?
Here are my entities:
//User.php
class User {
/**
* #ORM\OneToMany(targetEntity="Item", mappedBy="user", onDelete="CASCADE")
*/
private $items;
}
//Item.php
class Item {
/**
* #var User
*
* #ORM\ManyToOne(targetEntity="User", inversedBy="items")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
* })
*/
private $user;
/**
* #ORM\OneToMany(targetEntity="ItemImage", mappedBy="item",onDelete="CASCADE")
*/
protected $images;
}
//ItemImage.php
class ItemImage {
/* Setters and getter **/
/**
* #var Items
*
* #ORM\ManyToOne(targetEntity="Item", inversedBy="images")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="item_id", referencedColumnName="id")
* })
*/
private $item;
/**
* #ORM\PostRemove()
*/
public function removeUpload() {
unlink($this->getUploadDir() . '/' . $this->imageName);
}
}
My question is when a user is deleted, will all the items associated with user and images related to the items be deleted ? I also want the PostRemove() callback of Image entity be called when User is deleted ? Which option should I use, onDelete="cascade" or cascade={'remove'} for such cases?
onDelete='CASCADE' will add an SQL level ON DELETE CASCADE. So yes, the images will be deleted from the table. This is very efficient because the database handles the deletes.
To have the listener called cascade={'remove'} is necessary. This makes Doctrine fetch the object's graph, which is slower.
Both options may be specified at the same time. In this case Doctrine will act as described in the last paragraph, but DELETEs not coming from Doctrine will cascade correctly, too; no listeners will be called in this case obviously.
I would tend to specify cascade={'remove'} only in this case to avoid accidental DELETEs without the listener being called (if there is no cleanup task for unreferenced files).
Details can be found in the Doctrine documentation.

Adding a timestamp property to a relation in Symfony2

I am trying to model a simple comment system with 2 entities - User and Post. The User class has the regular properties, and the Post class looks like this:
class Post
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* User who posted activity
*
* #var User
* #ORM\ManyToOne(targetEntity="Acme\AppBundle\Entity\User")
*/
protected $user;
/**
* #var string $text
*
* #ORM\Column(name="text", type="string", length=255)
*/
private $text;
/**
* #var datetime $timestamp
*
* #ORM\Column(name="timestamp", type="datetime")
*/
private $timestamp;
}
I need to make a simple association - Users can favourite posts. The logical thing to do would be to add ManyToMany association in User class to Post like so
#ORM\ManyToMany(targetEntity="Post")
Which would then create a mapping table with user_id and post_id as primary keys. Now my problem is that I need to add a timestamp property to track when the user liked a post. One possibility is to create a new Entity "FavouritePost", map it to both User and Post class using ManyToOne association, and add a timestamp property to it.
1) Is this the right/only way to do it in Symfony2?
2) If I want to select the most recent 15 posts, and check if the currently logged in user has liked the posts, how can I write a query for that?
Any help appreciated, as I am drawing a blank on how to do it via Symfony
1) Yes you may create a jointure table, and so its FavouritePost entity.
2) You should use a Repository to create all special queries :
#ORM\Entity(repositoryClass="Acme\AppBundle\Repository\FavouritePostRepository")
Then it's easy Doctrine queries, like you will make much more.