Jump to content

Alternatives to entity inheritance


Recommended Posts

I am using API-Platform, Doctrine, and Symfony with entities that utilize class type inheritance to extend an abstract entity.

I've been running into one issue after another primarily related to the serialization process (serialization groups, parent annotations not propagated to the child, etc), and while I am sure user error on my part is part of the culprit, it appears that API-Platform and potentially Symfony and Doctrine don't fully support entity inheritance.  My reason for thinking so is the incredibly sparse amount of documentation on the subject and remarks on quite a few github issues posts and other blogs how it is "bad practice".

For instance, say I have Mouse, Cat, and Dog which all extend AbstractAnimal, and each has a bunch of common properties such as birthday, weight, etc, and methods such as eats(), sleeps(), etc.  Sorry in advance for using hypothetical entities but I don't think doing so distracts.  I like how inheritance allows me to keep all common properties in a single table, but can let that go.  More importantly, the subset mouse, cat, and dog table shares an ID from the animal table allowing me to associate all animals to some other table (i.e. many-to-one to person whether they are their pet or many-to-many to country whether they are native to a country), and to retrieve a list of animals and filter by some property or type as needed without a bunch of unions.

To me, this sounds like inheritance, but if the products I am using don't support it very well, it doesn't matter.

First question.  Is entity inheritance considered bad practice?  And even if not, is it common for frameworks to limit their level of support for them?

If so, what can I do about it?  Maybe favor composition over inheritance?  Okay, great, I now have a single animal table which makes all my SQL concerns issues go away and I am pretty confident that my serialization issues will also go away. All I need to do is inject each animal with some "thing" to make them a mouse, cat, or dog.

But what do I call this thing?  I've struggled with this topic for a while and asked the same question regarding how to deal with BarCharts, PieCharts, GaugeCharts, LineCharts, etc all being charts but all acting slightly differently, and never really came to any conclusion.

For a non-hypothetical scenario, I have BacnetGateway and ModbusGateway which extend AbstractGateway.  Okay, this one is easy and I change to just having a Gateway and inject either BacnetProtocol or ModbusProtocol.

For another non-hypothetical scenario, I have PhysicalPoint which represents some real environmental parameter, VirtualPoint which represents combining one or more PhysicalPoints or VirtualPoints, and TransformedPoint (feel free to provide a better name) which represents performing some time function such as integrating over a given time.  Currently, they all extend AbstractPoint, but if I was trying to do so with composition, I could inject PointType but don't think doing so makes sense.

For my hypothetical scenario, do I make a DNA interface and inject an Animal with DogDNA to get a dog?

I really need to get my head around this once and for all.  Thanks

Link to post
Share on other sites

Your composition is backwards.

Consider if you injected Cat/Dog/Mouse into Animal. It's a simple question but how would you type that property?

class Animal
{

	/**
	 * @var ???
	 */
	private $dna;

You can't. You'd have to make each animal itself inherit from some "Dna" thing. Which would make this all the more complicated.

But if you did composition the other way around, you would have

class Cat
{

	/**
	 * @var Animal
	 */
	private $animal;

 

Of course this isn't quite inheritance in the normal sense. It's the concept of inheritance but implemented using composition. Actual inheritance would be "class Cat extends Animal" and thus you'd have to somehow get Animal's data directly into Cat. Not sure if Symfony/Doctrine can do that, but if it were custom code then it wouldn't be hard (SELECT * FROM cat JOIN animal...).

Link to post
Share on other sites
6 hours ago, requinix said:

Your composition is backwards.

Consider if you injected Cat/Dog/Mouse into Animal. It's a simple question but how would you type that property?

I was envisioning something like the following where $dog = new Animal(new DogDna());.  I guess I still have the issue of inheritance at the DNA level as I would need a many-to-one relationship between animal and abstractDna, but hopefully less of an issue as it is a more isolated scope.

class Animal
{
    /**
     * @var DnaInterface
     */
    private DnaInterface $dna;

    public function __construct(DnaInterface $dna)
    {
        $this->dna = $dna;
    }
	
    public function run()
    {
        $this->dna->run();
    }
}

 

7 hours ago, requinix said:

But if you did composition the other way around, you would have


class Cat
{

	/**
	 * @var Animal
	 */
	private $animal;

Of course this isn't quite inheritance in the normal sense. It's the concept of inheritance but implemented using composition.

Humm,  Initially, thought it looked off, but agree it basically does what inheritance does.  How common is this approach?  It might solve my immediate issues and will investigate, but am a little concerned if not often used for good reasons.

7 hours ago, requinix said:

Actual inheritance would be "class Cat extends Animal" and thus you'd have to somehow get Animal's data directly into Cat. Not sure if Symfony/Doctrine can do that, but if it were custom code then it wouldn't be hard (SELECT * FROM cat JOIN animal...).

What I have now is more classic inheritance where Mouse extends AbstractAnimal, BacnetGateway extends AbstractGateway, and PhysicalPoint extends AbstractPoint.  Yes, Doctrine can do this, but as I said there appears to be shortcomings.   I can consider your concept of inheritance but implemented using composition approach, but would like to first consider a more traditional composition approach.  Is it possible with my Animal entity and if so how might it look?

Thanks

Link to post
Share on other sites
59 minutes ago, NotionCommotion said:

I was envisioning something like the following where $dog = new Animal(new DogDna());.

Injecting dog DNA into some animal sounds highly unethical...

Anyway, point is that you still end up creating this extra "Dna" stuff. So there's Animal, Cat, Dog, Mouse, DnaInterface, CatDna, DogDna, and MouseDna? Plus the coupling Cat <-> CatDna <-> DnaInterface and so on. It's all too much.

 

59 minutes ago, NotionCommotion said:

Humm,  Initially, thought it looked off, but agree it basically does what inheritance does.  How common is this approach?  It might solve my immediate issues and will investigate, but am a little concerned if not often used for good reasons.

Don't know how common.

At this point I would have started switching over to distinct business models. Keep Cat, Dog, Mouse, and Animal as separate entities with their own data but probably with some helper methods to deal with the relationships, then set up real Cat, Dog, Mouse, and Animal classes using normal inheritance practices.
That means you can do things like have Cat/Dog/Mouse constructors that take both a Cat/Dog/Mouse model and the Animal model (and you pass the Animal model up to the parent constructor) - or just take the Cat/Dog/Mouse and use some relationship getter to get the corresponding Animal.

Link to post
Share on other sites
9 hours ago, requinix said:

Injecting dog DNA into some animal sounds highly unethical...

Anyway, point is that you still end up creating this extra "Dna" stuff. So there's Animal, Cat, Dog, Mouse, DnaInterface, CatDna, DogDna, and MouseDna? Plus the coupling Cat <-> CatDna <-> DnaInterface and so on. It's all too much.

Where do you think all the Labadoodle cam from?  Regardless, agree it is way to much.  Was driving home last night and realized why some dislike too abstract of examples.  Attempting to model life itself with a couple classes?  Sure, we can extend a couple classes and have an academic run() and speak() method, but quickly breaks down.  And for composition, suppose I would need to inject about a billion objects to even pretend to be doing so.  Was hoping to add this before your response.  Can't totally blame myself, however.  Went online to look for examples and just about every one has to do with animals or vehicles.

9 hours ago, requinix said:

At this point I would have started switching over to distinct business models. Keep Cat, Dog, Mouse, and Animal as separate entities with their own data but probably with some helper methods to deal with the relationships, then set up real Cat, Dog, Mouse, and Animal classes using normal inheritance practices.
That means you can do things like have Cat/Dog/Mouse constructors that take both a Cat/Dog/Mouse model and the Animal model (and you pass the Animal model up to the parent constructor) - or just take the Cat/Dog/Mouse and use some relationship getter to get the corresponding Animal.

So, have Cat, Dog, Mouse, and Animal as entities, and Cat, Dog, Mouse, and Animal as real classes?  What is the difference?

I know you don't use Doctrine and probably not API Platform, and am not expecting any specific advice to those platforms (but, if someone else already uses them, please chime in!), however, do wish to model the entities so that I don't need to utilize inheritance.  Not saying that there is anything wrong with inheritance but I am experience issues when used with Doctrine and API Platform (annotation on the parent does not extend to the child class, can't get serializer groups working as desired, likely other deficiencies which I haven't yet discovered, total lack of documentation on the subject, and suspect that they are known bugs).

For the points (environmental data), Point is currently abstract and PhysicalPoint, VirtualPoint, and TransformedPoint all extend Point.  While you can't tell by the below ERD, the same thing with Source and Datanode as they have the same super-subset inherited structure.  Currently, if I request a collection of all Points, I would expect all to have the properties in Point as well as the properties of their specific sub-type.  This works fine when I utilized all custom code, but doesn't work fine when using API-Platform with the standard Doctrine implementation (see above for definition of "not fine").  Also, I believe a RESTful API should have separate endpoints for resources that do not share the same structure.

As an alternative structure, I am thinking that Point should be concrete (think that is the right word - not abstract with these child physical/etc concrete classes), but would be injected with some object (or objects) which would make it act like a physical, virtual, or transformed point.   Then when serializing, Point would be serialized and and I would either also serialize this injected object or just provide its resource link.  Maybe I am just not imaginative enough, but all I can think to call this injected object is something like PointType, and when I asked almost this exact same question, I realize it has been over two years and I still might not get it.  Is it just what I am calling it where I am astray and I should call it what does (i.e. maybe DataSourceProvider) and not some generic PointType?

Hopefully I do get it, but even so, seems like I still have the same issue not from a PHP perspective but from a SQL perspective.  Point would need to have column data_source_provider_id which would reference some single table which would contain the data to create a PhysicalDataSourceProvider, VirtualDataSourceProvider, or TransformedDataSourceProvider.  And that table would need to have both a reference to the class to be created and different data would likely (definitely for my case) be needed for each DataSourceProvider type.  So, I am back to inheritance?  Granted, I think I would be better off because each of these DataSourceProviders will be much more standalone.

Still, think I am missing something because (I assume) many people utilize polymorphic objects with Doctrine and using Doctrine's inheritance options is rather rare (I believe).  Ugg...

 

image.png.b9b1f298f8f84276c200cd8e7ef28b9f.png

Link to post
Share on other sites
5 hours ago, NotionCommotion said:

Where do you think all the Labadoodle cam from?

I mean it sounds unethical when it's a human doing the injecting.

 

5 hours ago, NotionCommotion said:

So, have Cat, Dog, Mouse, and Animal as entities, and Cat, Dog, Mouse, and Animal as real classes?  What is the difference?

Keeping the database stuff separate from the business logic stuff means you don't have to try to coerce Doctrine to make things work the way you want. The database models stay simple and use straightforward Doctrine stuff, then the business models are where you design the classes to work the way you want them to work.
Like the database stuff doesn't really do inheritance. You could set them up as just regular relationships. The business models are where you do real inheritance, as in "class Cat extends Animal". And you don't have to fight Doctrine to make that happen.

 

5 hours ago, NotionCommotion said:

I know you don't use Doctrine and probably not API Platform, and am not expecting any specific advice to those platforms (but, if someone else already uses them, please chime in!), however, do wish to model the entities so that I don't need to utilize inheritance.  Not saying that there is anything wrong with inheritance but I am experience issues when used with Doctrine and API Platform (annotation on the parent does not extend to the child class, can't get serializer groups working as desired, likely other deficiencies which I haven't yet discovered, total lack of documentation on the subject, and suspect that they are known bugs).

Then there ya go: don't do it, and make the inheritance happen in a different non-Doctrine place.

Link to post
Share on other sites

What does model, view, controller, business model, service, presenter, action, domain, responder, router, mapper, entity, database model all have in common?  They are all stuff!

I definitely have a hard time keeping them all straight.  Which is part of the problem.  This API-Platform thing takes care of all the CRUD business logic as well as generating OpenAPI definitions, and then there are hooks to perform application specific business.  I suppose I can find some other place to make the inheritance happen but then would be totally deviating from the intended approach.  Well, actually maybe I could do it in the normalizers and denormalizers.  Just seems that I should be doing it through composition.

Link to post
Share on other sites

Step 1. Create classes that only handle database stuff. Do what Doctrine says you should do for "inheritance". Worst case there's no real inheritance feature and what you do is standard relationship stuff (with foreign keys and such).

Step 2. Create classes that only handle the "application specific business" stuff. They use proper inheritance because that's what you do. They have whatever your application actually needs from them.

Step 3. Make those application classes use the database classes to get data. I mean like

abstract class BusinessAnimal
{

	private $animal;

	protected function __construct(DatabaseAnimal $animal)
	{
		$this->animal = $animal;
	}

}

class BusinessCat extends BusinessAnimal
{

	private $cat;

	public function __construct(DatabaseCat $cat)
	{
		parent::__construct($cat->animal());
		$this->cat = $cat;
	}

}

That's proper inheritance, BusinessCat can have whatever methods to do things, DatabaseCat can work however it needs to work for database stuff, and BusinessCat uses DatabaseCat extensively to do things.

Link to post
Share on other sites
On 3/13/2021 at 2:17 PM, requinix said:

 


class BusinessCat extends BusinessAnimal
{
	// ....
    public function __construct(DatabaseCat $cat)
	{
		parent::__construct($cat->animal());
		// ...
	}

}

 

DatabaseCat is the same thing as some would have called EntityCat, and some might have named DatabaseCat's animal() method as getAnimal(), true?

And as shown in your BusinessCat's constructor, DatabaseCat $cat contains an instance of DatabaseAnimal.

And to get it their in the first place, I would use composition:

$databaseAnimal = new DatabaseAnimal($dbAnimProp1, $dpAnimProp1);
$databaseCat = new DatabaseCat($dbCatProp1, $dbCatProp2, $databaseAnimal);

Just making sure I am on the right page.

Link to post
Share on other sites

Yes to the Database/Entity stuff, yes to the constructor.

Maybe yes to the composition. See what Doctrine wants you to do. The whole point of separating Database/Entity from Business is that your database classes don't need to fight against your ORM.
So the question is not what you think you should do but what Doctrine says you should do.

Link to post
Share on other sites
12 hours ago, requinix said:

So the question is not what you think you should do but what Doctrine says you should do.

Thank you requinix.  Good to know I got two YESs, one MAYBE, and no NOs!

It is not Doctrine I am fighting with.  Actually know it pretty well.  It is that confounding Symfony and associated bundles.  I expect I am in the minority in this position, but I focused on SQL first, then PHP, then Doctrine, and only recently Symfony.

For instance, I can easily have Doctrine generated SQL for class table inheritance or One-to-One.

Inheritance using CTI

CREATE TABLE animal (
  id SERIAL NOT NULL, 
  name VARCHAR(255) NOT NULL, 
  sex VARCHAR(255) NOT NULL, 
  weight INT NOT NULL, 
  birthday DATE NOT NULL, 
  color VARCHAR(255) NOT NULL, 
  type VARCHAR(32) NOT NULL, 
  PRIMARY KEY(id)
);
CREATE TABLE cat (
  id INT NOT NULL, 
  likes_to_purr BOOLEAN NOT NULL, 
  PRIMARY KEY(id)
);
ALTER TABLE 
  cat 
ADD 
  CONSTRAINT FK_CAT FOREIGN KEY (id) REFERENCES animal (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE;

Composition using One-to-One

CREATE TABLE animal (
  id SERIAL NOT NULL, 
  name VARCHAR(255) NOT NULL, 
  sex VARCHAR(255) NOT NULL, 
  weight INT NOT NULL, 
  birthday DATE NOT NULL, 
  color VARCHAR(255) NOT NULL, 
  PRIMARY KEY(id)
);
# animal_id is NOT NULL for Unidirectional and DEFAULT NULL for bidirectional
CREATE TABLE cat (
  id SERIAL NOT NULL, 
  animal_id INT DEFAULT NULL, 
  likes_to_purr BOOLEAN NOT NULL, 
  PRIMARY KEY(id)
);
CREATE UNIQUE INDEX UNIQ_CAT ON cat (animal_id);
ALTER TABLE 
  cat 
ADD 
  CONSTRAINT FK_CAT FOREIGN KEY (animal_id) REFERENCES animal (id) NOT DEFERRABLE INITIALLY IMMEDIATE;


I think the inheritance approach makes more sense, however, as I said API-Platform does not play well with inheritance and would like to consider the other approach.

One question is whether it makes sense to have an Animal as a property in a Cat, Dog, Mouse.  Animal while an entity and not abstract is not fully an animal, and is just part of the story.  Maybe I just got to let it go. But also, I can't for instance get the animal with ID #123 and retrieve a cat because animal doesn't contain the cat ID (unless I make the one-to-ones bidirectional in which animal would need a property for cat, dog, and mouse where all would be NULL except one which doesn't seem right). 

That is why I proposed my unethical DNA solution.  While I still have DB inheritance, my various DNA types don't share any properties other than an ID, so I don't "think" I will any problems with API-Platform.  And I can make all concrete DNA classes implement an interface so that Animal can contain one dna property and not a separate property for each type.

CREATE TABLE animal (
  id INT NOT NULL, 
  dna_id INT NOT NULL, 
  name VARCHAR(255) NOT NULL, 
  sex VARCHAR(255) NOT NULL, 
  weight INT NOT NULL, 
  birthday DATE NOT NULL, 
  PRIMARY KEY(id)
);
CREATE TABLE dna (
  id INT NOT NULL, 
  type VARCHAR(32) NOT NULL, 
  PRIMARY KEY(id)
);
CREATE TABLE cat_dna (
  id INT NOT NULL, 
  likes_to_purr BOOLEAN NOT NULL, 
  PRIMARY KEY(id)
);
CREATE TABLE dog_dna (
  id INT NOT NULL, 
  plays_fetch BOOLEAN NOT NULL, 
  doghouse_color VARCHAR(255) NOT NULL, 
  PRIMARY KEY(id)
);
CREATE UNIQUE INDEX UNIQ_ANIMAL_DNA_ID ON animal (dna_id);
ALTER TABLE 
  animal 
ADD 
  CONSTRAINT FK_DNA FOREIGN KEY (dna_id) REFERENCES dna (id) NOT DEFERRABLE INITIALLY IMMEDIATE;

I know Animal is a made up scenario, however, it is the same as my real inject a Point with a given type of data source to get a PhysicalPoint, VirtualPoint, or TransformedPoint.  Sorry in advance if I am being way too obtuse.

PS.  Your below example is totally obviously, however, that being said I've never done it and like it!  BusinessAnimal can deal with DatabaseAnimal and BusinessCat can deal with DatabaseCat without needing a bunch of intermediary getters.  Thanks!

class BusinessCat extends BusinessAnimal
{
    private $cat;

    public function __construct(DatabaseCat $cat)
    {
        parent::__construct($cat->animal());
        $this->cat = $cat;
    }
}

 

Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.