Using Doctrine2, I am trying to execute a findOneBy on an entity, where I have a joined a table using OneToOne, and I want to search for columns in the joined table.
The two PHP entities in play are (in simplified versions):
Page:
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="Page")
* #ORM\HasLifecycleCallbacks()
*/
class Page extends EntityInterface
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="boolean")
*/
public $isActive;
/**
* #ORM\OneToOne(targetEntity="\PageLocalization")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="id", referencedColumnName="pageId")
* })
**/
public $pageLocalization;
}
PageLocalization:
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="PageLocalization")
* #ORM\HasLifecycleCallbacks()
*/
class PageLocalization extends EntityInterface
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
public $pageId;
/**
* #ORM\Column(type="string")
*/
public $localeCode;
/**
* #ORM\Column(type="string")
*/
public $title;
}
The entities work and I can extract data just fine through e.g. $entityRepository->findOneBy(["id"=>1]).
Now, for instance, I want to search for Page.id = 1, Page.isActive = true and PageLocalization.localeCode = "en-US". How is this search performed?
My attempt below doesn't work.
$entityRepository->findOneBy([
"id" => 1,
"isActive" => true,
"pageLocalization" => [
"localeCode" => "en-US"
]
]);
I had no luck finding the answer through Google, Stackoverflow, or the Doctrine2 documentation.
The most frequent solutions I see is that people settle for are constructing the queries manually using $entityManager->createQueryBuilder(). However, I feel this approach defies the purpose of having the entities.
Is this type of search possible at all using purely annotations and entities?
From what I've gathered thus far, it is indeed not possible to perform a search like this across multiple entities (joined tables) using any of the EntityRepository's "find" methods; find, findAll, findBy, and findOneBy.
Stackoverflow answers to similar questions:
https://stackoverflow.com/a/16730168/1879194
https://stackoverflow.com/a/12258270/1879194
https://stackoverflow.com/a/19489147/1879194
The list goes on...
Conclusion
One must perform this type of searches using DQL or the QueryBuilder.
Thoughts
I find this limitation to be highly obnoxious and a serious feature inadequacy in Doctrine2.
If half the time, when I want to touch the database, I have to build the queries myself, why would I bother adhering to and maintaining two drastically different approaches; (1) an ORM with entities reflecting database tables and (2) constructing pure SQL requests through a DBAL, e.g. PDO or Doctrine's DQL?
Doctrine2 considerations (slightly off-topic)
I really want to learn how to work with and utilize Doctrine2 efficiently. And while Doctrine2 does have a grand array of nice features, it certainly also consists of some massive rebar walls, which one keeps running into face-first. Google and Stackoverflow certainly has a bunch of "Why can't I ...?" questions related to Doctrine2 and joining tables in particular.
Related
I'm developing a custom content management system with Symfony 5 and Doctrine.
I'm trying to implement a relation between the entities Document and Video (actually there are many more, but for simplicity sake let's say are just two) and the User entity.
The relation represent the User who wrote the document or recorded the video. So the relation here is called Author. Each document or video can have one or more author. Each User can have none or more document or video.
I would like to use just a single associative Author associative entity, like this:
entity_id|author_id|entity
Where:
entity_id: is the id of the document or video
author_id: is the user_id who authored the entity
entity: is a constant like document or video to know to which entity the relation refer to
The problem is that I cannot understand how to build this in Doctrine. Was this a classic SingleEntity<-->Author<-->Users relationship I would have build it as a ManyToMany item, but here it's different.
Author would probably contain two ManyToOne relations (one with the User entity and one with either the Document or the Video entity) plus the entity type field, but I really don't know how to code the "DocumentorVideo`" part. I mean:
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity=??????????, inversedBy="authors")
* #ORM\JoinColumn(nullable=false)
*/
private $entity; // Document or Video
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="articles")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
/**
* #ORM\Column(type="smallint")
*/
private $entityType;
How should I manage the first field?
Don't know if would be better to store it under two differents attributes. If not and mandatory, I think those "objects" should have a common interface or something, so take a look to doctrine inheritance that should fulfill your needs
My suggestion is to store the entity namespace Ex. Acme\Entity\Document in a property and the id in another and to use the entity manager to get the entity.
Edit: Though you won't have the relation, I prefer that way over others because it is reusable and the performance is rather the same. Also if I need to pass it to a JSON response, I just create a normalizer and I am good to go.
/**
* #ORM\Column(type="string")
*/
private $entityNamespace;
/**
* #ORM\Column(type="integer")
*/
private $entityId;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function getEntity()
{
return $this->em->getRepository($this->entityNamespace)->find($this->entityId);
}
I have created two entities of existing database tables, these tables use the doctrine conventions for table relationships, I need to relate the tables to be able to work, the entities work by consulting data, but not between them.
Table name "Articulos"
class Articulos
{
/**
* #ORM\Id()
* #ORM\Column(type="integer")
*/
private $ID_Articulo;
/**
* #ORM\Column(name="ID_Clasificacion_Articulo", type="integer")
* #ORM\ManyToOne(targetEntity="ClasificacionesArticulos")
*/
private $ID_Clasificacion_Articulo;
.......
Table name "ClasificacionesArticulos"
class ClasificacionesArticulos
{
/**
* #ORM\Column(name="ID_Clasificacion_Articulo", type="integer")
* #ORM\Id()
* #ORM\OneToMany(targetEntity="Articulos", mappedBy="ID_Clasificacion_Articulo")
*/
private $ID_Clasificacion_Articulo;
/**
* #ORM\Column(type="string", length=150)
*/
private $Codigo;
/**
* #ORM\Column(type="string", length=150)
*/
private $Nombre;
.........
When I consult any of the entities, returns result without children of relationships. I suppose it's because of the names of the fields id does not use the name strategies, but I can not change them in the database, I have to adapt to it by requirements.
If someone has an idea, I thank you very much
This can be accomplished by implementing custom Doctrine naming strategy. In Symfony entity, use camelCase to avoid any problems with naming. But, if you need specific table names follow the guide
You have to implement NamingStrategy class:
class CustomNamingStrategy implements NamingStrategy
{
}
register it as a service by adding following to the end of the the config/services.yaml :
app.naming_strategy.custom:
class: App\Service\CustomNamingStrategy
autowire: true
Then specify naming strategy by editing config/packages/doctrine.yaml as follows:
naming_strategy: app.naming_strategy.custom
I believe you are looking for propertyToColumnName method, as Doctrine says
If you have database naming standards, like all table names should be
prefixed by the application prefix, all column names should be lower
case, you can easily achieve such standards by implementing a naming
strategy.
I have an entity that stores large files as blobs to the DB.
I would now like to get Symfony to never ever load these blobs unless I specifically request them via the appropriate getter.
In essence I want the same idea as lazy-loading relationships but for a string property.
What I have tried so far is to put all my other properties that hold the file meta data into a trait and then apply that trait to two entities.
namespace App\Entity\Traits;
use Doctrine\ORM\Mapping as ORM;
trait DocumentMetaData
{
/**
* #var int|null
*
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var \DateTime|null
*
* #ORM\Column(type="datetime")
*/
private $date_uploaded;
}
One entity has nothing to it but the trait...
namespace App\Entity;
use App\Entity\Traits\DocumentMetaData;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Table(name="documents")
* #ORM\Entity()
*/
class Document
{
use DocumentMetaData;
}
...the other has the added blob property:
namespace App\Entity;
use App\Entity\Traits\DocumentMetaData;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Table(name="documents")
* #ORM\Entity()
*/
class DocumentFile
{
use DocumentMetaData;
/**
* #var string|null
*
* #ORM\Column(type="blob")
*/
private $blob;
}
Now, if I don't want the blob to be loaded, for example for a listing of files, I simply use the entity that doesn't have the blob.
This approach sort of works but causes issues as I need to point both entities at the same table (see the class level ORM annotations).
Specifically, it makes doctrine freak out when running migrations:
The table with name 'myapp.documents' already exists.
That makes perfect sense and really I'm hoping that someone can point me to a nicer solution.
How can I tell doctrine not to load the blob unless its explicitly asked for?
So as per the comments on my question - the way to do this so that migrations do not break is to leverage doctrine's ability to lazy load relationships between tables.
Basically I had to go and create a new entity that only holds my giant blobs and then establish a one to one relationship between the original entity and the blob entity.
I then set that relationship to load EXTRA_LAZY and as a result I can now control when precisely the blobs of giant data should be loaded.
I don't think this is ideal in terms of normalising the DB design but it works a lot better than anything else so happy with that.
I have a View entity that represents the primary page record, and then I have an associated entity called ViewVersion which stores multiple versions of the entity as it's changed over time. The View entity sets the current "Published" ViewVersion in the VersionId field. This makes for a simple OneToOne association. But in some contexts I will also want to get all the versions associated with this View entity, e.g. if I want to allow the user to review older versions and revert back. So I will need another mapping which is a OneToMany. The first viewVersion will map to the active "published" version, and the second viewVersions will show all the versions.
Entity Definitions
/**
* #ORM\Entity
* #ORM\Table(name="view")
* #ORM\Entity(repositoryClass="Gutensite\CmsBundle\Entity\View\ViewRepository")
*/
class View extends Entity\Base {
/**
* #ORM\OneToOne(targetEntity="\Gutensite\CmsBundle\Entity\View\ViewVersion", inversedBy="view", cascade={"persist", "remove"}, orphanRemoval=true)
* #ORM\JoinColumn(name="versionId", referencedColumnName="id")
*/
protected $viewVersion;
/**
* #ORM\Column(type="integer", nullable=true)
*/
protected $versionId = NULL;
/**
* #ORM\OneToMany(targetEntity="\Gutensite\CmsBundle\Entity\View\ViewVersion", mappedBy="viewAll", cascade={"persist", "remove"}, orphanRemoval=true)
*/
protected $viewVersions;
}
/**
* #ORM\Entity
* #ORM\Table(name="view_version")
* #ORM\Entity(repositoryClass="Gutensite\CmsBundle\Entity\View\ViewVersionRepository")
*/
class ViewVersion extends Entity\Base {
/**
* #ORM\OneToOne(targetEntity="\Gutensite\CmsBundle\Entity\View\View", mappedBy="viewVersion", cascade={"persist"})
*/
protected $view;
/**
* #ORM\ManyToOne(targetEntity="\Gutensite\CmsBundle\Entity\View\View", inversedBy="viewVersions")
* #ORM\JoinColumn(name="viewId", referencedColumnName="id")
*/
protected $viewAll;
/**
* The primary view entity that this version belongs to.
* #ORM\Column(type="integer", nullable=true)
*/
protected $viewId;
}
This "works" but is it recommended to have two associations with the same entity like this? Or is this a really bad idea?
The ViewVersion entity will reference a single View entity in both cases, but the mapped associations need two separate variables, e.g. View and ViewAll. I'm not exactly sure how the internals work for the association, and how the reference variable with the mapping is used.
Alternatively, I could get rid of the OneToOne association, and just set a ViewRepository function to get the current published version based on the versionId (just like the old mapped entity used to do with the getVersion()). That would work, but is it more internal overhead, because it would make two queries... or will Doctrine be smart enough to optimize this, just like it did with the getVersion().
NOTE:
These other answers are not complete.
References:
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-associations.html
http://doctrine-orm.readthedocs.org/en/2.0.x/reference/association-mapping.html#one-to-many-bidirectional
Typically, I have found the best approach is to solve this in a different way.
One common pattern I have seen before is you use a single table to hold all records, and have an 'active' flag.
If your query to select the active one works like so:
SELECT * FROM table WHERE active = true ORDER BY updated_at DESC LIMIT 1;
Then enabling a new one becomes as simple as:
UPDATE table SET active = 1, updated_at = '<timestamp>' WHERE id = <new id>;
UPDATE table SET active = 0, updated_at = '<timestamp>' WHERE id = <old id>;
Your new page will be active as soon as the first query hits, and your second query will avoid any sort of weirdness as that row will already be no longer active.
If you have other models that depend on a consistent ID to reference, then another route which also maintains some sanity would be to have one table for the active entries (in whole, not in part) and then a second table with additional metadata to track versions.
The latter approach could be nicely handled via Doctrine's inheritance system (http://docs.doctrine-project.org/en/2.0.x/reference/inheritance-mapping.html) which would let you define the base View class, and then for the "ViewRevision" model, extend View and add a "Revised on" type timestamp.
Per the advice from #jmather I've decided this model is "okay", because I need a single View entity that other entities can access (e.g. Routing urls that point to a single View, i.e. "page").
I've changed the OneToOne relationship for View to be unidirectional only, because the ViewVersion already has an association back to the View via the other OneToMany (so it doesn't need two paths back).
This allows me to keep a simple method for $view->getPublished() handy and seems more logical.
/**
* #ORM\Entity
* #ORM\Table(name="view")
*/
class View extends Entity\Base {
/**
* This is a OneToOne Unidirectional association, just so that we can get the
* current published version easily, based on the publishedId.
* #ORM\OneToOne(targetEntity="\Gutensite\CmsBundle\Entity\View\TestVersion")
* #ORM\JoinColumn(name="publishedId", referencedColumnName="id")
*/
protected $published;
/**
* #ORM\Column(type="integer", nullable=true)
*/
protected $publishedId = NULL;
/**
* This is the regular OneToMany Bi-Directional Association, for all the versions.
* #ORM\OneToMany(targetEntity="\Gutensite\CmsBundle\Entity\View\ViewVersion", mappedBy="view", cascade={"persist", "remove"}, orphanRemoval=true)
*/
protected $versions;
}
/**
* #ORM\Entity
* #ORM\Table(name="view_version")
*/
class ViewVersion extends Entity\Base {
/**
* #ORM\ManyToOne(targetEntity="\Gutensite\CmsBundle\Entity\View\View", inversedBy="versions")
* #ORM\JoinColumn(name="viewId", referencedColumnName="id")
*/
protected $view;
/**
* The primary view entity that this version belongs to.
* #ORM\Column(type="integer", nullable=true)
*/
protected $viewId;
}
However, I've discovered that as long as the $view->publishedId is set the view can't be deleted from the database because of foreign key constraints (even though it's uni-directional). So I have to break that foreign key link before removing. I think that's fine. I posted details about that here: Overlapping Entity Association causing Database Foreign Key Constraint Errors when Removing Entity
(Sorry for my incoherent question: I tried to answer some questions as I was writing this post, but here it is:)
I'm trying to create a database model with a many-to-many relationship inside a link table, but which also has a value per link, in this case a stock-keeping table. (this is a basic example for more problems I'm having, but I thought I'd just test it with this before I would continue).
I've used exportmwb to generate the two Entities Store and Product for this simple example, both are displayed below.
However, the problem now is that I can't figure out how to access the stock.amount value (signed int, as it can be negative) using Doctrine. Also, when I try to create the tables using doctrine's orm:schema-tool:create function
This yielded only two Entities and three tables, one as a link table without values and two data tables, as many-to-many relationships aren't entities themselves so I can only have Product and Store as an entity.
So, logically, I tried changing my database model to have stock as a separate table with relationships to store and product. I also rewrote the fieldnames just to be able to exclude that as a source of the problem:
Then what I found was that I still didn't get a Stock entity... and the database itself didn't have an 'amount'-field.
I really needed to be able to bind these stores and products together in a stock table (among other things)... so just adding the stock on the product itself isn't an option.
root#hdev:/var/www/test/library# php doctrine.php orm:info
Found 2 mapped entities:
[OK] Entity\Product
[OK] Entity\Store
And when I create the database, it still doesn't give me the right fields in the stock table:
So, looking up some things here, I found out that many-to-many connections aren't entities and thus cannot have values. So I tried changing it to a separate table with relationships to the others, but it still didn't work.
What am I doing wrong here?
A Many-To-Many association with additional values is not a Many-To-Many, but is indeed a new entity, since it now has an identifier (the two relations to the connected entities) and values.
That's also the reason why Many-To-Many associations are so rare: you tend to store additional properties in them, such as sorting, amount, etc.
What you probably need is something like following (I made both relations bidirectional, consider making at least one of them uni-directional):
Product:
namespace Entity;
use Doctrine\ORM\Mapping as ORM;
/** #ORM\Table(name="product") #ORM\Entity() */
class Product
{
/** #ORM\Id() #ORM\Column(type="integer") */
protected $id;
/** ORM\Column(name="product_name", type="string", length=50, nullable=false) */
protected $name;
/** #ORM\OneToMany(targetEntity="Entity\Stock", mappedBy="product") */
protected $stockProducts;
}
Store:
namespace Entity;
use Doctrine\ORM\Mapping as ORM;
/** #ORM\Table(name="store") #ORM\Entity() */
class Store
{
/** #ORM\Id() #ORM\Column(type="integer") */
protected $id;
/** ORM\Column(name="store_name", type="string", length=50, nullable=false) */
protected $name;
/** #ORM\OneToMany(targetEntity="Entity\Stock", mappedBy="store") */
protected $stockProducts;
}
Stock:
namespace Entity;
use Doctrine\ORM\Mapping as ORM;
/** #ORM\Table(name="stock") #ORM\Entity() */
class Stock
{
/** ORM\Column(type="integer") */
protected $amount;
/**
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="Entity\Store", inversedBy="stockProducts")
* #ORM\JoinColumn(name="store_id", referencedColumnName="id", nullable=false)
*/
protected $store;
/**
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="Entity\Product", inversedBy="stockProducts")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id", nullable=false)
*/
protected $product;
}
Doctrine handles many-to-many relationships just fine.
The problem that you're having is that you don't need a simple ManyToMany association, because associations can't have "extra" data.
Your middle (stock) table, since it contains more than product_id and store_id, needs its own entity to model that extra data.
So you really want three classes of entity:
Product
StockLevel
Store
and two associations:
Product oneToMany StockLevel
Store oneToMany StockLevel