Relationships such as one-to-one, one-to-many (or many-to-one), and many-to-many are familiar to database programmers. Laravel's Eloquent has brought these concepts into an object-oriented environment. Additionally, Eloquent has even more powerful tools such as polymorphic relations, where entities can be related to more than one other entity. In the following examples, we will see the relationship between accommodations, rooms, and amenities.
The first relation is one-to-one. In our example software, we can use the example of a room in our accommodation. A room may only (at least easily) belong to one accommodation, so the room belongs to the accommodation. Inside the Room
Eloquent model, the following code tells Eloquent that the room belongs to the accommodation
function:
class Room extends Eloquent { public function accommodation() { return $this->belongsTo('MyCompanyAccommodation'), } }
Sometimes, the database tables do not follow the active record pattern, especially if the programmer inherits a legacy database. If the database used a table called bedroom
instead of rooms
, then the class would add an attribute to indicate the table name:
class Room extends Eloquent { protected $table = 'bedroom'; }
When the following route code is executed, then the accommodation
object will be returned as a JSON object:
Route::get('test-relation',function(){ $room = MyCompanyAccommodationRoom::find(1); return $room->accommodation; });
The response will be as follows:
{"id":9,"name":"LovelyHotel","description":"Lovely Hotel Greater Pittsburgh","location_id":1,"created_at":null,"updated_at": "2015-03-13 22:00:23","deleted_at":null}
One common mistake is to use the following command:
return $room->accommodation();
In this case, the programmer expects to return the model. This will return the actual belongsTo
relation and in the context of the RESTful API, there will be an error thrown:
Object of class IlluminateDatabaseEloquentRelationsBelongsTo could not be converted to string
This is because Laravel can convert the JSON object to a string, but not a relationship.
The SQL run is as follows:
select * from rooms where rooms.id = '1' limit 1 select * from accommodations where accommodations.id = '9' limit 1
Eloquent tends to favor multiple simpler queries as opposed to doing larger joins.
First the room is found. Then, limit 1
is added because find
is only used to find a single entity or row. Once the accommodation_id
is found, the next query will find the accommodation with that corresponding ID and return the object. If the active record pattern was followed, the SQL that Eloquent produces is extremely readable.
The second relation is one-to-many. In our example software, we can use the example of an accommodation having many rooms. Since rooms may belong to one accommodation, then the the accommodation has many rooms. Inside the Accommodation
Eloquent model, the following code tells Eloquent that the accommodation has many rooms.
class Accommodation { public function rooms(){ return $this->hasMany('MyCompanyAccommodationRoom'), } }
In a similar route, the following code is run. This time, a collection of rooms
objects will be returned as JSON-formatted objects inside an array:
Route::get('test-relation',function(){ $accommodation = MyCompanyAccommodation::find(9); return $accommodation->rooms; });
The response will be the following array:
[{"id":1,"room_number":0,"created_at":null,"updated_at":null, "deleted_at":null,"accommodation_id":9},{"id":3,"room_number": 12,"created_at":"2015-03-14 08:52:25","updated_at":"2015-03-14 08:52:25","deleted_at":null,"accommodation_id":9},{"id":6, "room_number":12,"created_at":"2015-03-14 09:03:36","updated_at":"2015-03-14 09:03:36","deleted_at":null,"accommodation_id":9},{"id": 14,"room_number":12,"created_at":"2015-03-14 09:26:36","updated_at":"2015-03- 1409:26:36","deleted_at":null,"accommodation_id":9}]
The SQL run is as follows:
select * from accommodations where accommodations.id = ? limit 1 select * from rooms where rooms.accommodation_id = '9' and rooms.accommodation_id is not null
As before, the accommodation is found. The second query will find the rooms that belong to that accommodation. A check is added to confirm that the accommodation_id
is not null.
In our example software application, the relationship between amenity and room is many-to-many. Each room can have many amenities, such as Internet access and a Jacuzzi, and each amenity is shared among many rooms: every room in an accommodation could and should have internet access! The following code, which uses a belongsToMany
relationship, enables an amenity to belong to many rooms:
class Amenity { public function rooms(){ return $this- >belongsToMany('MyCompanyAccommodationRoom'), } }
The test route, which tells us how each room has a certain amenity, is written as follows:
Route::get('test-relation',function(){ $amenity = MyCompanyAccommodationAmenity::find(3); return $amenity->rooms; });
A list of rooms is returned:
[{"id":1,"room_number":0,"created_at":2015-03-14 08:10:45,"updated_at":null,"deleted_at":null, "accommodation_id":9},{"id":5,"room_number":12, "created_at":"2015-03-14 09:00:38","updated_at":"2015-03-14", 09:00:38","deleted_at":null,"accommodation_id":12}, ...]
The SQL executed is as follows:
select * from amenities where amenities.id = ? limit 1 select rooms.*, amenity_room.amenity_id as pivot_amenity_id, amenity_room.room_id as pivot_room_id from rooms inner join amenity_room on rooms.id = amenity_room.room_id where amenity_room.amenity_id = 3
We recall the belongToMany
relationship that returns the rooms that have a particular amenity:
class Amenity { public function rooms(){ return $this- >belongsToMany('MyCompanyAccommodationRoom'), } }
Eloquent skillfully gives us the corresponding belongsToMany
relationship to determine which amenities a particular room has. The syntax is exactly the same:
class Room { public function amenities(){ return $this- >belongsToMany('MyCompanyAccommodationAmenity'), } }
The test route is virtually the same, just substituting amenities
for rooms
:
Route::get('test-relation',function(){ $room = MyCompanyAccommodationRoom::find(1); return $room->amenities; });
The result is a list of amenities for the room with ID 1:
[{"id":1,"name":"Wifi","description":"Wireless Internet Access","created_at":"2015-03-1409:00:38","updated_at":"2015-03-14 09:00:38","deleted_at":null},{"id":2,"name": "Jacuzzi","description":"Hot tub","created_at":"2015-03-14 09:00:38","updated_at":null,"deleted_at":null},{"id":3,"name": "Safe","description":"Safe deposit box for protecting valuables","created_at":"2015-03-1409:00:38","updated_at": "2015-03-1409:00:38","deleted_at":null}]
The query used is as follows:
select * from rooms where rooms.id = 1 limit 1 select amenities.*, amenity_room.room_id as pivot_room_id, amenity_room.amenity_id as pivot_amenity_id from amenities inner join amenity_room on amenities.id = amenity_room.amenity_id where amenity_room.room_id = '1'
The query, substituting room_id
for amenity_id
and rooms
for amenities
, is clearly parallel.
One great feature of Eloquent is "has-many-through". What if the requirements of the software change and we are asked to group some of the accommodations together into franchises? If an application user would like to search for a room, any of the rooms in any of the accommodations that belong to that franchise could be found. A franchises table will be added, and a nullable column to the accommodations table called franchise_id
will be added. This will optionally allow for an accommodation to belong to a franchise. Rooms already belong to accommodations through the accommodation_id
column.
A room belongs to an accommodation
through its accommodation_id
key, while an accommodation belongs to a franchise through its franchise_id
key.
Eloquent allows us to retrieve the rooms associated to a franchise by using hasManyThrough
:
<?php namespace MyCompany; use IlluminateDatabaseEloquentModel; class Franchise extends Model { public function rooms() { return $this- >hasManyThrough('MyCompanyAccommodationRoom', 'MyCompanyAccommodation'), } }
The hasManyThrough
relationship takes the target or the "has" as its first parameter (in this case, the room) and its "through" as the second parameter (in this case, the accommodation).
The logic stated as a phrase is: This franchise has many rooms through its accommodations.
Using the previous test route, the code is written as follows:
Route::get('test-relation',function(){ $franchise = MyCompanyFranchise::find(1); return $franchise->rooms; });
The rooms are returned as an array as would be expected:
[{"id":1,"room_number":0,"created_at":null,"updated_at":null,"deleted_at":null,"accommodation_id":9,"franchise_id":1}, {"id":3,"room_number":12,"created_at":"2015-03-14 08:52:25","updated_at":"2015-03-14 08:52:25","deleted_at":null,"accommodation_id":9, "franchise_id":1},{"id":6,"room_number":12,"created_at":"2015-03-14 09:03:36","updated_at":"2015-03-14 09:03:36","deleted_at":null,"accommodation_id":9, "franchise_id":1}, ]
The queries executed are as follows:
select * from franchises where franchises.id = ? limit 1 select rooms.*, accommodations.franchise_id from rooms inner join accommodations on accommodations.id = rooms.accommodation_id where accommodations.franchise_id = 1