I've recently started using Zend Framework (1.8.4), to provide admin tools for viewing the orders of a shopping cart site.
What I'd like to do is to efficiently create multiple model (Zend_Db_Table_Row_Abstract) objects from a single database result row.
The relationship is simple:
an Order has one Customer (foreign key order_custid=customer.cust_id);
a Customer has many Orders.
Loading the orders is easy enough. Using the method documented here:
Modeling objects with multiple table relationships in Zend Framework
...I could then grab the customer for each.
foreach ($orderList as $o)
{
cust = $o->findParentRow('Customers');
print_r ($cust); // works as expected.
}
But when you're loading a long list of orders - say, 40 or more, a pageful - this is painfully slow.
Next I tried a JOIN:
$custTable = new Customers();
$orderTable = new Orders();
$orderQuery = $orderTable->select()
->setIntegrityCheck(false) // allows joins
->from($orderTable)
->join('customers', 'cust_id=order_custid')
->where("order_status=?", 1); //incoming orders only.
$orders = $orderTable->fetchAll($orderQuery);
This gives me an array of order objects. print_r($orders) shows that each of them contains the column list I expect, in a protected member, with raw field names order_* and cust_*.
But how to create a Customer object from the cust_* fields that I find in each of those Order objects?
foreach ($orders as $o) {
$cols = $o->toArray();
print_r ($cols); // looks good, has cust_* fields...
$cust = new Customer(array( 'table' => 'Customer', 'data' => $cols ) );
// fails - $cust->id, $cust->firstname, etc are empty
$cust->setFromArray($cols);
// complains about unknown 'order_' fields.
}
Is there any good way to create an Order and a Customer object simultaneously from the joined rows? Or must I run the query without the table gateway, get a raw result set, and copy each of the fields one-by-one into newly created objects?
Zend_Db doesn't provide convenience methods to do this.
Hypothetically, it'd be nifty to use a Facade pattern for rows that derive from multiple tables. The facade class would keep track of which columns belong to each respective table. When you set an individual field or a whole bunch of fields with the setFromArray() method, the facade would know how to map fields to the Row objects for each table, and apply UPDATE statements to the table(s) affected.
Alternatively, you could work around the problem of unknown fields by subclassing Zend_Db_Table_Row_Abstract, changing the __set() behavior to silently ignore unknown columns instead of throwing an exception.
You can't have an OO interface to do everything SQL can do. There must be some line in the sand where you decide a reasonable set of common cases have been covered, and anything more complex should be done with SQL.
I use this method to assign database row fields to objects. I use setter methods, but this could probably be also done with only properties on object.
public function setOptions(array $options){
$methods = get_class_methods($this);
foreach ($options as $key => $value) {
$method = 'set' . ucfirst($key);
if (in_array($method, $methods)) {
$this->$method($value);
}
}
return $this;
}
Related
What I am trying to do
I want to query a specific set of records using active model like so
$jobModel = Jobs::find()->select('JOB_CODE')->distinct()->where(['DEPT_ID'=>$dept_id])->all();
Then I want to assign a flag attribute to the records in this activerecord based on whether they appear in a relationship table
What I have tried
So in my job model, I have declared a new attribute inAccount. Then I added this function in the job model that sets the inAccount flag to -1 or 0 based on whether a record is found in the relationship table with the specified account_id
public function assignInAccount($account_id){
if(JobCodeAccounts::find()->where(['JOB_CODE'=>$this->JOB_CODE])->andWhere(['ACCOUNT_ID'=>$account_id])->one() == null){
$this->inAccount=0;
}
else{
$this->inAccount = -1;
}
}
What I have been doing is assigning each value individually using foreach like so
foreach($jobModel as $job){
$job->assignInAccount($account_id);
}
However, this is obviously very slow because if I have a large number of records in $jobModel, and each one makes a db query in assignInAccount() this could obviously take some time if the db is slow.
What I am looking for
I am wondering if there is a more efficient way to do this, so that I can assign inAccount to all job records at once. I considered using afterFind() but I don't think this would work as I need to specify a specific parameter. I am wondering if there is a way I can pass in an entire model (or at least array of models/model-attributes and then do all the assignations running only a single query.
I should mention that I do need to end up with the original $jobModel activerecord as well
Thanks to scaisEdge's answer I was able to come up with an alternative solution, first finding the array of jobs that need to be flagged like so:
$inAccountJobs = array_column(Yii::$app->db->createCommand('Select * from job_code_accounts where ACCOUNT_ID = :account_id')
->bindValues([':account_id' => $account_id])->queryAll(), 'JOB_CODE');
and then checking each job record to see if it appears in this array like so
foreach($jobModel as $job){
if(in_array($job->JOB_CODE, $inAccountJobs))
$job->inAccount = -1;
else
$job->inAccount = 0;
}
Does seem to be noticeably faster as it requires only a single query.
I'm looking to a better way to write this function.
It's inside a Doctrine Entity Model
public function getCompanySubscriptions()
{
foreach ($this->subscriptions as $key => $value) {
if ($value->getPlan()->getType() == 'E' && $value->getActive()) {
return $value;
}
}
return null;
}
$this->subscriptions is a many-to-one collection and can have different "types" (but only one of them with type "E").
Problem is: If the Company has too many $subscriptions this function will be too slow to return that only one of type "E" which I need to check when building the view with TWIG. The solution would be to use a QueryBuilder, but I haven't found a way to use it directly from the entity model.
You cannot use a QueryBuilder inside your entity, but instead you can use doctrine Criteria for filtering collections (on SQL level). Check the documentation chapter 8.8. Filtering Collections for more details on Criteria.
If the collection has not been loaded from the database yet, the filtering API can work on the SQL level to make optimized access to large collections.
For example to only get active subscriptions:
$subscriptions = $this->getCompanySubscriptions();
$criteria = Criteria::create()
->where(Criteria::expr()->eq("active", true));
$subscriptions = $subscriptions ->matching($criteria);
Like that you can solve your performance issues, since the collection is loaded from the database using the conditions from the Criteria directly.
The problem in your case might be that you need to join on Plan, but joining is not possible in a Criteria. So if joining is really necessary then you should consider using a custom query where you do the join with conditions in your company EntityRepository (for example with a QueryBuilder).
Note.
The foreach in your question can be rewritten using the filter method from the ArrayCollection class. The filter takes a predicate and all elements satisfying the predicate will be returned.
Look also here in the doctrine 2 class documentation for more info.
Your predicate would look something like:
$predicate = function($subscription){
$subscription->getPlan()->getType() == 'E' && $subscription->getActive();
}
and then:
return $this->subscriptions->filter($predicate);
I want to develop a RESTful API based on Slim Framework and Doctrine 2. I have a detailed permission management. So I have permissions defined as:
role:admin|entity:person|column:name|write:1
Im considering the most effective way to implement the right management into the web service.
Therefore I need to filter a computed subset of columns when building my query. What is the best place to do that, still enabling me to use all the default methods like findAll() etc. I could of course filter my fields like below:
$all = Article::createQuery('a')->getArrayResult();
/*this is getting ALL the fields -it would be better to filter before
retrieving from the db
*/
$allFiltered = array();
foreach($all as $index=>$article){
$filteredArticle = new Article();
foreach($user->getPermission('Article','r') as $permission){
$column = $permission->column;
$filteredArticle->$column = $article->$column;
}
$allFiltered[$index]=$filteredArticle
)
$app->response->setBody(json_encode($all));
Is there a way to do this at one place for all retrieving methods ?
Doctrrine2 can select partial object doc
You can fetch all permitted columns and just concatenate with a query ...
I'm building a product management tool where the product can have an arbitrary number of attributes, documents, features, images, videos as well as a single type, brand, and category. There are a few other related tables, but this is enough to demonstrate the problem.
There's a Model class called ProductModel that contains a method like this (reduced for clarity):
public function loadValues() {
//Product entity data
$this->id = $this->entity->getId();
$this->slug = $this->entity->getSlug();
// One of each of these
$this->loadType();
$this->loadBrand();
$this->loadCategory();
// Arbitrary number of each of these
$this->loadAttributes();
$this->loadDocuments();
$this->loadFeatures();
$this->loadImages();
$this->loadVideos();
...
}
Each of the load methods does some boiler plate that eventually executes this method:
public function loadEntitiesByProductId($productId=0) {
// Get all the entities of this type that are associated with the product.
$entities = $this->entityManager
->getRepository($this->entityName)
->findByProduct($productId);
$instances = array();
// Create a Model for each entity and load the data.
foreach ($entities as $entity) {
$id = $entity->getId();
$instances[$id] = new $this->childClass();
$instances[$id]->entity = $entity;
$instances[$id]->loadValues();
}
return $instances;
}
This is OK for cases where the related entity is a single table, but usually it's a mapper. In those cases, I get all the mapper entities in the first query then I have to query for the related entity within the loadValues() method (via Doctrine's get<Entity>() method). The result of this process is a huge number of queries (often >100). I need to get rid of the extraneous queries, but I'd like to do so without losing the idioms I'm using across my data models.
Is there a way to get the entityManager to do a better job at using joins to group these queries?
There were a couple problems with my previous approach:
First, I was getting the entities from the repository instead of loading them from the existing entity:
$entities = $this->entityManager
->getRepository($this->entityName)
->findByProduct($productId);
Better is:
$method = $this->deriveGetMethod($this->entityName);
$entities = $productEntity->$method()
Second, I was retrieving the product entity using $this->entityManager->getRespository... which works fine for loading small data sets (a single table or one or two relations), but there's no way to get the repository's findBy methods to load relations in a single query. The solution is to use the queryBuilder.
$qb = $this->entityManger->createQueryBuilder();
$query = $this->select('product',/*related tables*/)->/*joins etc.*/
$productEntity = $query->getSingleResult();
I started using Laravel yesterday, the ORM seems powerful. Does it have any way of updating rows in related models? This is what I tried:
Step 1: Generate a JSON object with the exact structure the database has. The JSON object has certain fields that are subarrays which represent relationships in the database.
Step 2: Send the JSON object via POST to Laravel for processing, here it gets tricky:
I can change the JSON object into an array first
$array = (array) $JSONobject;
Now I need to update, I would expect this to work:
Product::update($JSONobject->id,$array);
But because the array has subarrays, the update SQL that is executed cannot find the sub-array column in the table, it should instead look for the associated table. Can this be done? Or do I have to call the other models as well?
Thanks in advance!
This is something that Eloquent does not handle for you. The array that you supply to the update() method should contain columns only for, in your case, the Product model. You might try something like this to update relations. This is all off the top of my head and is by no means tested. Take it with a grain of salt.
$update = (array) $JSONobject;
$relations = [];
foreach ($update as $column => $value)
{
// If the value is an array then this is actually a relation. Add it to the
// relations array and remove it from the update array.
if (is_array($value))
{
$relations[$column] = $value;
unset($update[$column]);
}
}
// Get the product from the database so we can then update it and update any of the
// the products relations.
$product = Product::find($update['id']);
$product->update($update);
foreach ($relations as $relation => $update)
{
$product->{$relation}()->update($update);
}
The above code assumes that the key for your nested relation arrays is the name of the relation (method name used in your model). You could probably wrap this up in a method on your Product model. Then just call something like Product::updateRecursively($JSONobject); I'm terrible with names but you get the idea.
This probably won't work with more complex relations either. You'd have to take it a few steps further for things like many to many (or probably even one to many).