I often write database intensive applications and discovered the gateway pattern which seemed to fit my needs. My problem now is that many of my models are compositioned of other models.
For example I have an User
model and an Order
model which each has an own Gateway
.
Interface for User
Gateway:
interface IUserGateway {
public function create(User $user) : User;
public function query($limit = -1, $offset = 0) : Array;
public function update(User $user) : User;
public function delete(User $user) : boolean;
public function findById(int $id) : User;
}
Interface for Order
Gateway:
interface IOrderGateway {
public function create(Order $user) : Order;
public function query($limit = -1, $offset = 0) : Array;
public function update(Order $user) : Order;
public function delete(Order $user) : boolean;
public function findById(int $id) : Order;
}
Now the Order
contains an instance of a User
which then is referenced inside my database with a foreign index orders.user_id
inside the orders
table.
The question now is how I query the and create the objects inside the IOrderGateway
implementation.
Until now I used Dependency Injection
to inject IUserGateway
instances in the constructor
of my IOrderGateway
implementation and then call IUserGateway::findById
to get the required User
instance for each Order
.
But this seems like a waste of resources because I have to do an additional query to database (MySQL in this case) which also could be accomplished by an INNER JOIN
.
But then the IOrderGateway
has to care about User
s creation.
I've also did some more research and found implementations of the Gateway Pattern
which only return the raw row-data from the data layer and then pass it to a Factory
which creates the object.
So which method should I use? Are there better solutions than the mentioned ones?
When I don't use a JOIN
I only have the user_id
for each order. To get the instance of the user I have to call IUserGateway::findById()
to get that instance which causes an additional query to the database.
The problem is I don't know what solution to use. I'm relatively new to OO-Concepts. For me using a JOIN
to query and then instantiate Order
and User
seems to be the better solution. But then I would violate the SOLID
principles because the IOrderGateway
cares about Order
and User
.
2 Answers 2
When I don't use a
JOIN
I only have theuser_id
for each order. To get the instance of the user I have to callIUserGateway::findById()
to get that instance which causes an additional query to the database.
All true. This is how CRUD works.
If you want the JOIN, some ORM's allow you to do something like this:
var result = db.ExecuteQuery<MyDataTransferObject>("[sql with join goes here]", parameters)
Where MyDataTransferObject
is a class containing properties whose names correspond to the columns you want to return from your database.
Lazy Loading of Entity Relationships
If you are writing your own ORM/data access layer, what you are really looking for is Lazy Loading of Entity Relationships. This isn't really something a Gateway explicitly handles. A "proxy" class could do this (which might take another Gateway as a constructor argument).
First, a few entity classes that you already have/look similar to what you have:
class User
{
public function __construct($username, $id = 0) {
$this->username = $username;
$this->id = $id;
}
private $id;
private $username;
public function getId() {
return $this->id;
}
public function getUsername() {
return $this->username;
}
}
class Order
{
private $user;
public function setUser(User $user) {
$this->user = $user;
}
}
The trick with "proxies" in this case is they should inherit from the real entity. If you want to lazy load the User
object, create the UserProxy
class and have it inherit from User
. Then UserProxy
must support the same public methods that User
does:
class UserProxy : User
{
public function __construct(IUserGateway $gateway, $userId) {
$this->id = $id;
$this->gateway = $gateway;
}
private $gateway;
private $id;
private $entity;
public function getId() {
// No need to hit the database when we already have the User Id.
// This value does not need "lazy loading".
return $this->id;
}
public function getUsername() {
// The username is not a primary key column, so now we fetch this
// from the database. This value is "lazy loaded".
return this->getEntity()->getUsername();
}
private function getEntity() {
if (!isset($this->entity)) {
// This does the "lazy loading" of the User data
$this->entity = $this->gateway->findById($this->id);
}
return $this->entity;
}
}
In order for things to be wired up right, you should create an IGatewayFactory
interface, which will give you access to any "gateway" that you need:
interface IGatewayFactory
{
public getUserGateway() : IUserGateway;
}
Now we have enough to integrate this with the OrderGateway
class:
class OrderGateway implements IOrderGateway
{
public function __construct(IGatewayFactory $factory) {
$this->factory = $factory;
}
private $factory;
public function findById($id) {
$data = // get from database...
$order = new Order();
$user = new UserProxy($this->factory->getUserGateway(), $data['user_id']);
// This works, because the UserProxy object is implicitly cast to
// its parent class: User
$order->setUser($user);
return $order;
}
}
Now for an example use case:
$orderGateway = new OrderGateway(new GatewayFactory());
$order = $orderGateway->findById(3);
$user = $order->getUser(); // Returns a UserProxy object
echo $user->getId(); // Returns the cached value in memory
echo $user->getUsername(); // Now we hit the database to fetch the record
But this still doesn't address the issue of the extra trip to the database. We just delay this trip until it is needed (another good buzzword to search for is N+1 query performance).
It really sounds like you want an Object/Relational Mapper (ORM). Searching for php orm should give you a good place to start. ORM libraries give you oodles of options for querying data, as well as inserting, updating and deleting data.
Once you reach the point of lazy loading data and needing to do JOIN's, you have outgrown your home brewed data access solution. An ORM isn't overcomplicating things. It sounds like in your case an ORM is simply admitting that the problem is bigger than it originally seemed. It is, in fact, a right sized solution.
The question now is how I query the and create the objects inside the IOrderGateway implementation.
-- Um, the same way you would if you weren't using anIOrderGateway
implementation?But this seems like a waste of resources because I have to do an additional query to database
-- Why is an additional query required?So which method should I use?
-- Which one most effectively meets your specific requirements?