I use Codeigniter with datamapper orm and have a problem
this are my models:
mailing -> has many row
row -> has many cell
cell -> has many version
version has one created and one updated field.
I want to get the last 10 mailings order by last version changes created or updated..
I thought to do it like this:
$versions = new Version();
now get last 10 versions order by created or updated
and distinct by mailing_id and now get all mailings to show...
like this: ?
foreach ($version as $v)
{
$v->mailing->get();
}
thx for helping
Yes, you can call ->get() on every related model inside a loop but this would generate a n+1 query scenario and be slow if you are looping over lots of version rows.
You can use the include_related to get full Mailing instances loaded with data when you query Versions in one step (with a join behind a curtain) like this:
$versions = new Version;
$versions->order_by(...)->limit(...); // add your ordering and limiting as before
$versions->include_related('mailing', null, true, true);
// include related mailings, with of their fields and create instances, see
$versions->get();
foreach ($versions as $version) {
// now the $version->mailing is a Mailing instance loaded with the related data
print $version->mailing->id
}
Related
I am trying to build a custom sorting for the product listings in shopware 6.
I want to include a foreign table (entity is: leasingPlanEntity), get the min of one of the fields of that table (period_price) and then order the search result by that value.
I have already built a Subscriber, and try it like that, what seems to work.
public static function getSubscribedEvents(): array
{
return [
//ProductListingCollectFilterEvent::class => 'addFilter'
ProductListingCriteriaEvent::class => ['addCriteria', 5000]
];
}
public function addCriteria(ProductListingCriteriaEvent $event): void
{
$criteria = $event->getCriteria();
$criteria->addAssociation('leasingPlan');
$criteria->addAggregation(new MinAggregation('min_period_price', 'leasingPlan.periodPrice'));
// Sortierung hinzufügen.
$availableSortings = $event->getCriteria()->getExtension('sortings') ?? new ProductSortingCollection();
$myCustomSorting = new ProductSortingEntity();
$myCustomSorting->setId(Uuid::randomHex());
$myCustomSorting->setActive(true);
$myCustomSorting->setTranslated(['label' => 'My Custom Sorting at runtime']);
$myCustomSorting->setKey('my-custom-runtime-sort');
$myCustomSorting->setPriority(5);
$myCustomSorting->setFields([
[
'field' => 'leasingPlan.periodPrice',
'order' => 'asc',
'priority' => 1,
'naturalSorting' => 0,
],
]);
$availableSortings->add($myCustomSorting);
$event->getCriteria()->addExtension('sortings', $availableSortings);
}
Is this already the right way to get the min(periodPrice)? Or is it taking just a random value out of the leasingPlan table to define the sort-order?
I didn't find a way, to define the min_period_price aggregate value in the $myCustomSorting->setFields Methods.
Update 1
Some days later, I asked a less complex question in the shopware community on slack:
Is it possible to use the DAL to define a subquery for an association in the product-listing?
It should generate something like:
FROM
JOIN (
SELECT ... FROM ... WHERE ... GROUP BY ... ORDER BY ...
) AS ...
The answer there was:
Don't think so
Update 2
I also did an in-deep anlysis of the DAL-Query-Builder, and it really seems to be not possible, to perform a subquery with the current version.
Update 3 - Different approach
A different approach might be, to define custom fields in the main entity. Every time a change is made on the main entity, the values of this custom fields should be recalculated.
It is a lot of overhead work, to realize this. Especially when the fields you are adding, are dependend on other data like the availability of a product in the store, for example.
So check, if it is worth the extra work. Would be better, to have a solution for building subqueries.
Unfortunately it seems that in your case there is no easy way to achieve this, if I understand the issue correctly.
Consider the following: for each product you can have multiple leasingPlan entities, and I assume that for a given context (like a specific sales channel or listing) that still holds. This means that you would have to sort the leasingPlan entities by price, then take the one with the lowest price, and then sort the products by their lowest-price leasingPlan's price.
There seems to be no other way to achieve that, and unfortunately for you, sorting is applied at the end, even if it is sort of a subquery.
So, for example, if you have the following snippet
$criteria = $event->getCriteria();
$criteria->addAssociation('leasingPlan');
$criteria->getAssociation('leasingPlan')
->addSorting(new FieldSorting('price', FieldSorting::ASCENDING))
->setLimit(1)
;
The actual price-sorting would be applied AFTER the leasingPlan entities are fetched - essentially the results would be sorted, meaning that you would not get the cheapest leasing plan per product, instead getting the first one.
You can only do something like that with filters, but in this case there is nothing to filter by - I assume you don't have one leasingPlan per SalesChannel or per language, so that you could limit that list to just one entry that could be used for sorting
That is not to mention that this could not be included in a ProductSortingEntity, but you could always work around that by plugging into the appropriate events and modifying the criteria during runtime
I see two ways to resolve your issue
Making another table which would store the cheapest leasingPlan per product and just using that as your association
Storing the information about the cheapest leasingPlans in e.g. cache and using that for filtering (caution: a mistake here would probably break the sorting, for example if you end up with too few or too many leasingPlans per product)
public function applyCustomSorting(ProductListingCriteriaEvent $event): void
{
// One leasingPlan per one product
$cheapestLeasingPlans = $this->myCustomService->getCheapestLeasingPlanIds();
$criteria = $event->getCriteria();
$criteria->addAssociation('leasingPlan');
$criteria->getAssociation('leasingPlan')
->addSorting(new FieldSorting('price', FieldSorting::ASCENDING))
->addFilter(new EqualsAnyFilter('id', $cheapestLeasingPlans))
;
}
And then you could sort by
$criteria->addSorting(new FieldSorting('leasingPlan.periodPrice', FieldSorting::ASCENDING));
There should be no need to add the association manually and to add the aggregation to the criteria, that should happen automatically behind the scenes if your custom sorting is selected in the storefront.
For more information refer to the official docs.
At the moment i have a 1 to many relationship between 2 models. A Machine model and a Document Model.
A document can have multiple versions and i always related to my machine. Now i only want to show the documents with the latest or highest version.
I've tried to get the version from the document but then i can only return 1 document to my view instead of returning multiple documents. I've also tried to get the documents the following way but i get stuck with the steps i need to take after creating the loop:
$machine = Machine::find($id);
$documents = $machine->documents;
foreach($documents as $document)
{
}
dd($documents);
return view('machine.detail', compact('machine'));
So to specify my question: How can i return only the unique documents with the latest version to my view. So if file 1 has versions 1,2,3,10 and file 2 has versions 1,2,3 i want to return version 10 of file 1 and version 3 of file 2.
If I understand your question correctly you can do the following.
// Add this relation also on your model where your hasMany is on properly Machine.php
public function latestDocument()
{
return $this->hasOne(Document::class)
->latest('version_column');
}
// Controller
$machine = Machine::with(['latestDocument'])->findOrFail($id);
$document = $machine->latestDocument;
return view('machine.detail', compact('machine'));
If you have a unique key for each document that is shared for all the versions something like this should work.
$documents = $machine->documents->groupBy('name of your unique key')->map(function($documents) {
$latestVersion = $documents->max('version');
return $documents->firstWhere('version', $latestVersion);
});
Explanation: The get the latest version from each document we first need to group all documents by a key to separate all the unqiue documents into multiple collections that contain the versions.
Then we need to map them into a new collection where we only get the document with the latest version.
Note: Dont forget to change $machine = Machine::find($id); to $machine = Machine::with('documents')->find($id); so you load the documents before hand. This will prevent the n+1 query problem
Here is a very basic example of what I want to do. The code I have come up with seems quite verbose... ie looping through the collection, etc.
I am using a Telerik MVC grid that posts back a collection of deleted, inserted and updated ViewModels. The view models are similar but not exactly the same as the entity.
For example... I have:
Order.Lines. Lines is an entity collection (navigation property) containing OrderDetail records. In the update action of my controller using the I have a List names DeletedLines pulled from the POST data. I also have queried the database and have the Order entity including the Lines collection.
Now I basically want to tell it to delete all the OrderDetails in the Lines EntityCollection.
The way I have done it is something like:
foreach (var line in DeletedLines) {
db.DeleteObject(Order.Lines.Where(l => l.Key == line.Key).SingleOrDefault())
}
I was hoping there was a way that I could use .Interset() to get a collection of entities to delete and pass that to DeleteObject.. however, DeleteObject seems to only accept a single entity rather than a collection.
Perhaps the above is good enough.. but it seemed like there should be an easier method.
Thanks,
BOb
Are the items in DeletedLines attached to the context? If so, what about this?
foreach (var line in DeletedLines) db.DeleteObject(line);
Response to comment #1
Ok, I see now. You can make your code a bit shorter, but not much:
foreach (var line in DeletedLines) {
db.DeleteObject(Order.Lines.SingleOrDefault(l => l.Key == line.Key))
}
I'm not sure if DeleteObject will throw an exception when you pass it null. If it does, you may be even better off using Single, as long as you're sure the item is in there:
foreach (var line in DeletedLines) {
db.DeleteObject(Order.Lines.Single(l => l.Key == line.Key))
}
If you don't want to re-query the database and either already have the mapping table PK values (or can include them in the client call), you could use one of Alex James's tips for deleting without first retrieving:
http://blogs.msdn.com/b/alexj/archive/2009/03/27/tip-9-deleting-an-object-without-retrieving-it.aspx
I want to get the newest entries for each of my threads (private messaging system) with Propel 1.6 making use of the fluid ModelQuery interface. This would allow me to reuse both methods for getting newest entries and only getting entries where a user is involved (nobody wants to see messages not for him).
I already found out that in standard-SQL I have to use a subquery to get the newest entry for each of my forum threads. I also found out that in Propel you have to use a Criteria::CUSTOM query to achieve this, but the whole Criteria::CUSTOM stuff seems to be pre-Propel-1.6, because none of the examples makes use of the new ModelQuery.
Now the problem is, that I want to make use of the concenation feature in ModelQueries, where you can attach several own methods to each other like this:
$entries = MessageQuery::create()
->messagesInvolvingUser($user) // user retrieved or sent the message
->newestFromThread() // get the latest entry from a lot of Re:-stuff
I do not think that this would still be possible if I had to use
$c = new Criteria();
$c->add([the subquery filter]);
in newestFromThread().
What’s the best method to retrieve the latest entry for each thread given the following scheme (thread_id means that all messages belong to the same correspondence, I want only one entry per thread_id):
id(INT)
title(VARCHAR)
thread_id(INTEGER)
date(DATETIME)
The current PHP-implementation looks like this:
<?php
class MessageQuery extends BaseMessageQuery {
public function messagesInvolvingUser($user) {
return $this
->where('Message.AuthorId = ?', $user->getId())
->_or()
->where('Message.RecipientId = ?', $user->getId());
}
public function newestFromThread() {
return $this;
// To be implemented
}
}
And I am using it like this:
$messages = MessageQuery::create()
->messagesInvolvingUser(Zend_Auth::getInstance()->getIdentity())
->newestFromThread()
->find();
How about ordering results by date (DESC) and to limit to one result ?
Considering the answers in a similar question about pure SQL solutions, I guess it is easiest to add a new column newest indicating which message in a communcation is the newest. This probably fits the object-oriented approach of Propel better, too. I could write my application like this then:
public function preInsert(PropelPDO $con = null) {
$this->setNewest(1);
$this->getEarlier()->setNewest(0);
return true;
}
I'm encountering an issue because I'm sure I'm not doing this correctly with my programming. I have created a custom model in Magento.
In the database table of my model there are several entities with the same attributes...
I need to pick just one from all these entities with the same attribute that I have. For the moment I did this:
$myvariable = Mage::getModel('test/test')->getCollection()
->setOrder('idserialkeys', 'asc')
->addFilter('idproduit', 1)
->addFilter('utilise', 0)
->addFilter('customerid', 0)
->addFilter('numcommande', 0)
From this loading I have around a hundred results but I need to update only one of these, so just after I'm doing:
->setPageSize(1);
The problem is that I need a foreach after to update my entity
foreach($mavaribale as $modifiemoi) {
// Update of my entity because of course there is only one
}
As you can see I'm obliged to do a loop (for each) even if I have a setPagesize... I would like to avoid this loop to optimize my code.
When you have a collection, and you only need one element, use the getFirstItem method. Try this:
$modifiemoi = $myvariable->getFirstItem();
Make sure that you also use your setPageSize call so that you only transfer data for one item.
All collections are Varien_Data_Collection objects so you can use getFirstItem:
$modifiemoi = $mavaribale->getFirstItem();