I have a simple question. I saw a Symfony2-Tutorial where the blogger used everywhere simple entities without relations and was also using doctrine native-query to do joins and query the different entities.
What is better, to use doctrine native query or build every time relationships ?
Greetings Michael
I think that it depends on how the resultset will be handle. For example, suppose the following relationship:
a company has a lot of employees (1:N)
Just figure out the impact if this company has more than 10K employees when it does a select query in every request loading all employees data to be showed. In this scenario could be a good practice to create a lazy association in models:
<?php
/**
* #Entity
*/
class Company
{
/**
* #OneToMany(targetEntity="Employee", fetch="EXTRA_LAZY")
*/
public $employees;
}
At this case, doctrine only will triggered the required data from database, because it knows that all the data will be accessed gradually as you request it. You can read more about lazy associations in doctrine's docs.
How about the native queries?
Native queries can map arbitrary SQL code to objects, such as highly vendor-optimized SQL or stored-procedures. Fast scalar results and less ram usage. Take note that a complex model relationship could be too heavy for server to be manipulated. For example, look this structure based on Class Table Inheritance:
There is a super class called Product and there are more than 200 different sub-products that extends from Product. Each sub-product is storaged in its respective table.
<?php
abstract class Product
protected $name;
Some sub-products as example:
<?php
class Candy extends Product
/** specific property for this product */
private $sugarLevel;
Another one:
<?php
class IceCream extends Product
/** specific property for this product */
private $temperature;
Now, you need to assess every product in you depot. Normally, the first idea to get this resulst is doing:
$assess = array();
$products = $em->getRepository('models\Product');
foreach ($products as $p)
{
//summarize each product by type
$assets[$p->getType()] = $assets[$p->getType()] + 1;
}
echo "There are " . $assets['candy'] " candies in stock";
This is really heavy process because we are quering 200 tables just to determinate the existence of each product. This could be easy mitigated with a simple native query:
$query = $em->createNativeQuery('SELECT p.type, count(p.type) as total FROM Product p group by p.type', $rsm);
$result = $query->getArrayResult();
print_r($result);
// [0] => array('type' => 'candy', 'total' => 545),
// [1] => array('type' => 'icecream', 'total' => 344),
//...
// [199] => array('type' => 'foo', 'total' => 878),
Related
I'm creating a search function for my Company entity and putting my logic in a custom entity repository using Symfony 2 and Doctrine ORM.
The Company entity has a child collection Locations, as comapnies can have multiple locations, and a Company Name field.
I'm wondering if it's possible to search by Company Name and Location[1].address, Location[2].address, etc. using the Query Builder.
This is what I have so far and can't find a good resource for learning this.
*/ AppBundle/Repository/CompanyRepository.php */
class CompanyRepository extends EntityRepository
{
public function findBySearch($options)
{
$query = $this->createQueryBuilder('c');
foreach ($options as $key => $value) {
$query->where('c.' . $key . ' LIKE :' . $key);
$query->setParameter($key, '%' . $value . '%');
}
return $query->getQuery()->getResult();
}
}
It looks like this doesn't work:
$query->where('c.locations.city LIKE $city);
An example for the $options argument in findBySearch() would be:
[
'companyName' => 'Mikes Company',
'locations.city' => 'New York',
'locations.state' => 'New York'
]
Any advice or links to resources are much appreciated.
You selected c, after that you want to find company with some location, it mean that in company entity you should have (with type Location)field location. Location entity shoud have field (string)city. Field location in Company entity should have oneToMany association with LocationEntity. After that when you will add company's name you also will add company's location. And now you can write:
$query->leftJoin('c.locations', 'locations');
$query->where('locations.city LIKE :city');
$query->setParameter('city', '%'.$city.'%')
If you want to use c.locations.city property of location object you should call there association. Like leftJoin, innerJoin. But I think innerJoin here will be more better and take just two line.
$query->innerJoin('c.locations', 'locations', 'WITH', 'locations.city LIKE (:city)')
$query->setParameter('city', '%'.$city.'%')
try changing $query->where to $query->andWhere
->andWhere() can be used directly, without any ->where() before
I have created a Customer DataObject by extending Member. Customer has a many_many data relation with a Package DataObject.
I would like increment/decrement a Credits field in the Customer DataObject when a Package is linked / unlinked through the CMS based on the Limit field in the Package table.
Customer
class Customer extends Member {
private static $db = array(
'Gender' => 'Varchar(2)',
'DateOfBirth' => 'Date',
'Featured' => 'Boolean',
'Credits' => 'Int'
);
private static $many_many = array(
'Packages' => 'Package'
);
public function getCMSFields() {
$fields = new FieldList();
$config = GridFieldConfig_RelationEditor::create();
$config->removeComponentsByType('GridFieldAddNewButton');
$packageField = new GridField(
'Packages',
'Package',
$this->Packages(),
$config
);
$fields->addFieldToTab('Root.Package', $packageField);
Session::set('SingleID', $this->ID);
$this->extend('updateCMSFields', $fields);
return $fields;
}
}
Package
class Package extends DataObject {
private static $db = array(
'Title' => 'Varchar(255)',
'Limit' => 'Int'
);
private static $belongs_many_many = array(
'Customers' => 'Customer'
);
}
When you create or delete many to many relationship just one record is modified in your database - the one in table which joins elements of both sides of the relationship. Therefore neither object the relationship is based on is updated. This is why methods like: onBeforeWrite, onAfterWrite, onBeforeDelete and onAfterDelete will not be called at all and you cannot use them to detect such change.
However, Silverstripe provides class ManyManyList which is responsible for all operations connected to many to many relationships. There are two methods which are of your interest: add and remove. You can override them and put inside action to do what you need. These methods are obviously called on each link or unlink operation no matter object types are, so you should make some filtering on classes you are particularly interested in.
The proper way to override the ManyManyList class is to use Injector mechanism, so as not to modify anything inside the framework or cms folder. The example below uses relationship between Members and Groups in Silverstripe but you can easily adopt it to your need (Customer -> Member; Package -> Group).
app.yml
Injector:
ManyManyList:
class: ManyManyListExtended
ManyManyListExtended.php
/**
* When adding or removing elements on a many to many relationship
* neither side of the relationship is updated (written or deleted).
* SilverStripe does not provide any built-in actions to get information
* that such event occurs. This is why this class is created.
*
* When it is uses together with SilverStripe Injector mechanism it can provide
* additional actions to run on many-to-many relations (see: class ManyManyList).
*/
class ManyManyListExtended extends ManyManyList {
/**
* Overwritten method for adding new element to many-to-many relationship.
*
* This is called for all many-to-many relationships combinations.
* 'joinTable' field is used to make actions on specific relation only.
*
* #param mixed $item
* #param null $extraFields
* #throws Exception
*/
public function add($item, $extraFields = null) {
parent::add($item, $extraFields);
if ($this->isGroupMembershipChange()) {
$memberID = $this->getMembershipID($item, 'MemberID');
$groupID = $this->getMembershipID($item, 'GroupID');
SS_Log::log("Member ($memberID) added to Group ($groupID)", SS_Log::INFO);
// ... put some additional actions here
}
}
/**
* Overwritten method for removing item from many-to-many relationship.
*
* This is called for all many-to-many relationships combinations.
* 'joinTable' field is used to make actions on specific relation only.
*
* #param DataObject $item
*/
public function remove($item) {
parent::remove($item);
if ($this->isGroupMembershipChange()) {
$memberID = $this->getMembershipID($item, 'MemberID');
$groupID = $this->getMembershipID($item, 'GroupID');
SS_Log::log("Member ($memberID) removed from Group ($groupID)", SS_Log::INFO);
// ... put some additional actions here
}
}
/**
* Check if relationship is of Group-Member type.
*
* #return bool
*/
private function isGroupMembershipChange() {
return $this->getJoinTable() === 'Group_Members';
}
/**
* Get the actual ID for many-to-many relationship part - local or foreign key value.
*
* This works both ways: make action on a Member being element of a Group OR
* make action on a Group being part of a Member.
*
* #param DataObject|int $item
* #param string $keyName
* #return bool|null
*/
private function getMembershipID($item, $keyName) {
if ($this->getLocalKey() === $keyName)
return is_object($item) ? $item->ID : $item;
if ($this->getForeignKey() === $keyName)
return $this->getForeignID();
return false;
}
}
The solution provided by 3dgoo should also work fine but IMO that code does much more "hacking" and that's why it is much less maintainable. It demands more modifications (in both classes) and needs to be multiplied if you would like to do any additional link/unlink managing, like adding custom admin module or some forms.
The problem is when adding or removing items on a many to many relationship neither side of the relationship is written. Therefore onAfterWrite and onBeforeWrite is not called on either object.
I've come across this problem before. The solution I used isn't great but it was the only thing that worked for me.
What we can do is set an ID list of Packages to a session variable when getCMSFields is called. Then when an item is added or removed on the grid field we refresh the CMS panel to call getCMSFields again. We then retrieve the previous list and compare it to the current list. If the lists are different we can do something.
Customer
class Customer extends Member {
// ...
public function getCMSFields() {
// Some JavaScript to reload the panel each time a package is added or removed
Requirements::javascript('/mysite/javascript/cms-customer.js');
// This is the code block that saves the package id list and checks if any changes have been made
if ($this->ID) {
if (Session::get($this->ID . 'CustomerPackages')) {
$initialCustomerPackages = json_decode(Session::get($this->ID . 'CustomerPackages'), true);
$currentCustomerPackages = $this->Packages()->getIDList();
// Check if the package list has changed
if($initialCustomerPackages != $currentCustomerPackages) {
// In here is where you put your code to do what you need
}
}
Session::set($this->ID . 'CustomerPackages', json_encode($this->Packages()->getIDList()));
}
$fields = parent::getCMSFields();
$config = GridFieldConfig_RelationEditor::create();
$config->removeComponentsByType('GridFieldAddNewButton');
$packageField = GridField::create(
'Packages',
'Package',
$this->Packages(),
$config
);
// This class needs to be added so our javascript gets called
$packageField->addExtraClass('refresh-on-reload');
$fields->addFieldToTab('Root.Package', $packageField);
Session::set('SingleID', $this->ID);
$this->extend('updateCMSFields', $fields);
return $fields;
}
}
The if ($this->ID) { ... } code block is where all our session code happens. Also note we add a class to our grid field so our JavaScript refresh works $packageField->addExtraClass('refresh-on-reload');
As mentioned before, we need to add some JavaScript to reload the panel each time a package is added or removed from the list.
cms-customer.js
(function($) {
$.entwine('ss', function($){
$('.ss-gridfield.refresh-on-reload').entwine({
reload: function(e) {
this._super(e);
$('.cms-content').addClass('loading');
$('.cms-container').loadPanel(location.href, null, null, true);
}
});
});
})(jQuery);
Inside the if($initialCustomerPackages != $currentCustomerPackages) { ... } code block there are a number of things you can do.
You could use $this->Packages() to fetch all the current packages associated to this customer.
You could call array_diff and array_merge to get just the packages that have been added and removed:
$changedPackageIDs = array_merge(array_diff($initialCustomerPackages, $currentCustomerPackages), array_diff($currentCustomerPackages, $initialCustomerPackages));
$changedPackages = Package::get()->byIDs($changedPackageIDs);
The above code will add this functionality to the Customer side of the relationship. If you also want to manage the many to many relationship on the Package side of the relationship you will need to add similar code to the Package getCMSFields function.
Hopefully someone can come up with a nicer solution. If not, I hope this works for you.
note: Didn't actually check does the model work but by visually checking this should help you:
On the link you provided you are using
$customer = Customer::get()->Filter...
That returns a DataList of objects, not a singular object unless you specify what is the object you want from the DataList.
If you are filtering the Customers then you want to get a SPECIFIC customer from the DataList, e.g. the first one in this case.
$customer = Customer::get()->filter(array('ID' => $this->CustomerID))->first();
But You should be able to get the singular DataObject with:
$customer = $this->Customer();
As you are defining the Customer as "has_one". If the relation was a Has many, using () would get you a DataList of objects.
Protip:
You don't need to write our own debug files in SilverStripe. It has own functions for it. For example Debug::log("yay"); what writes the output to a file and Debug::dump("yay") that dumps it directly out.
Tip is that you can check what is the object that you accessing right. Debug::dump(get_class($customer)); would output only the class of the object.
I am developing a basic web-app in my job. I have to work with some sql server views. I made the decision of trying native queries, and once tested it's functionality, try to write some classes to code all the queries and kinda forget their implementation.
So my issue is, I've got an Entity in Acme/MyBundle/Entity/View1.php.
This entity has got all the attributes matching the table and also it's getters and setters.
I guess this entity is well mapped to the DB (Doctrine cant work with views easily).
My aim is to let a Controller be able to fetch some data from those views(SQL SERVER) and return it to the view (twig) so it can display the info.
$returned_atts = array(
"att1" => $result[0]->getAttribute1(), //getter from the entity
"att2" => $result[1]->getAttribute2(), //getter from the entity
);
return $returned_atts;`$sql = "SELECT [Attribute1],[Attribute2],[Attribute3] FROM [TEST].[dbo].[TEST_VIEW1]"; //THIS IS THE SQL SERVER QUERY
$rsm = new ResultSetMapping($em); //result set mappin object
$rsm->addEntityResult('Acme\MyBundle\Entity\View1', 'view1'); //entity which is based on
$rsm->addFieldResult('view1', 'Attribute1', 'attribute1'); //only choose these 3 attributes among the whole available
$rsm->addFieldResult('view1', 'Attribute2', 'attribute2');
$rsm->addFieldResult('view1', 'Attribute3', 'attribute3');
//rsm built
$query = $em->createNativeQuery($sql, $rsm); //execute the query
$result = $query->getResult(); //get the array
It should be possible to return the array straight from the getResult() method isn't it?
And what's killing me, how can I access the attribute1, attriute2 and attriute2?
$returned_atts = array(
"att1" => $result[0]->getAttribute1(), //getter from the entity
"att2" => $result[1]->getAttribute2(), //getter from the entity
);
return $returned_atts;`
If you want result as array, you don't need to use ResultSetMapping.
$sql = " SELECT * FROM some_table";
$stmt = $this->getDoctrine()->getEntityManager()->getConnection()->prepare($sql);
$stmt->execute();
$result = $stmt->fetchAll();
That is a basic example for controller action. You can dump the result, use var_dump(), to see how to access your particular field values.
More examples here Doctrine raw sql
On Symfony2 I have an entity called Product which is managed by Doctrine2 and being related with a single category and location.
Class Product {
protected $id;
protected $title;
// n additional fields
protected $productCategory;
protected $productDepot;
}
What I need to achieve is, select category and location COUNT's in a single query.
So I'm executing the following COUNT select with some groupings on the following DQL.
$this->em->getRepository('MyCoreBundle:Product')
->createQueryBuilder('P')
->addSelect('IDENTITY(P.productCategory) AS category_id')
->addSelect('IDENTITY(P.productDepot) AS location_id')
->addSelect("COUNT(P.id) AS tag_count")
->addGroupBy('P.productCategory')
->addGroupBy('P.productDepot')
->getQuery()
->getScalarResult();
However when it is executed, the whole result is as follows:
$result = [
0 => [
P_id => ...,
P_title => ...,
P_stock => ....,
/*
* Many many additional fields of the entity
*/
category_id => 15,
location_id => 40,
tag_count => 20
],
...
];
However obviously I just need category_id, location_id and tag_count fields. Since the table is a huge one (like everyone has one, nowadays), I'm trying to minimize footprint of the query as much as possible.
What I tried so far is adding a partial field of id (as follows) however it didn't make sense.
$this->em->getRepository('MyCoreBundle:Product')
->createQueryBuilder('P')
->addSelect('partial P.{id}')
->addSelect('IDENTITY(P.productCategory) AS category_id')
->addSelect('IDENTITY(P.productDepot) AS location_id')
->addSelect("COUNT(P.id) AS tag_count")
->addGroupBy('P.productCategory')
->addGroupBy('P.productDepot')
->getQuery()
->getScalarResult();
Am I trying to achieve something impossible, or do you have any idea to get just the aggregation fields (or with just an additonal ID field) of an an entity?
I currently have a simple one to many relationship between products and multiple deals (a table of 1 million deals in total) associated with the products.
What I'm trying to do is loop through the top 10 products and select the top deals relating to the product.
What would be the best way to achieve this in Doctrine 2? I was contemplating adding a method such as getTopDeals within the product entity, and then calling it within twig as I looped through each product like so:
{% for product in popular_products %}
{% set deal = product.getTopDeal() %}
{{ product.title }} - {{ deal.title }}, {{deal.price }}
{% endfor %}
However, I've read that generally it is frowned upon adding methods such as this into models, so I'm at an end as to what the best way to do this is.
Make a method in your Deals repository to accept a parameter and return the topdeal. In your controller, array_map() your products to produce an array of deals keyed by product. Then pass the deals array along with your products array to your template.
edit: sample requested:
Repository:
public function getTopDealProduct($productid)
{
$em=$this->getEntityManager();
$qb = $em->getRepository('Bundle:Deal')->createQueryBuilder('d');
$qb->join('d.product', 'p');
$qb->setMaxResults(1);
$qb->addOrderBy('d.price');
$query = $qb->getQuery();
$results = $query->getResult();
return $results;
}
Controller:
public function s2JUsorAction(Request $request, $id)
{
$dealrep = $this->em->getRepository('Bundle:Deal');
$prodrep = $this->em->getRepository('Bundle:Product');
$products= $prodrep->getProducts(); // Not shown here, write this
$deals= array_map(function($element) use ($dealrep){
return $dealrep->getTopDealProduct($element->getId());
}
,$products);
return $this->render('Bundle:Product:Deal.html.twig', array(
'products' => $products
,'deals' => $deals
));
}
The best practice is, "fat models, thin controllers". The logic for selecting the top deals for a product definitely has a place on the model, if the model itself is capable of doing this filtering, eg. it only needs the deal objects, which it has a relation with. For this, you could use the Criteria API, something like:
use Doctrine\Common\Collections\Criteria;
class Product {
private $deals; // many-to-many to Products
public function getTopDeals() {
$criteria = Criteria::create()->orderBy(array('price', 'DESC'))->setMaxResults(10);
return $this->deals->matching($criteria);
}
}
If the selection logic is more complicated, and needs to reach into the entity manager, then it is better suited for placing on an EntityRepository.