Problem/Motivation
In #3000931: Connection::query() does not support 'target' option we identified that there are places where DB api calls are made passing a ['target' => 'replica'] entry in the $options array.
This is a bug since the DB api cannot do anything with that, the required connection needs to be identified beforehand and the operation executed on it.
Proposed resolution
Use the @database.replica instead.
Remaining tasks
TBD
User interface changes
none
API changes
none
Data model changes
none
| Comment | File | Size | Author |
|---|---|---|---|
| #41 | interdiff_29-41.txt | 9.67 KB | mondrake |
| #41 | 3001216-replica-service-41.patch | 12.17 KB | mondrake |
Comments
Comment #2
volegerJust initial patch to show the problem. I'm not sure that the database factory defined properly, so any review and patches are welcome.
Comment #3
volegerComment #5
volegerUpdate test with the added argument in the constructor.
Comment #6
longwaveFix copy paste error
Comment #7
mondrakeComment #8
tstoecklerI like this issue in that it makes working with the replica database easier (or possible ;-)). I wonder, though, since we are already using
Database::getConnection()as a factory for thedatabaseservice, wouldn't it be easier for consuming code to define adatabase.replicaservice, that directly yields the replica connection? What do you think?Comment #10
andypostThe default database factory
no reason to inject database & factory same time
$connection = $this->connectionFactory->getDatabseConnection($accurate ? 'default' : 'replica')more descriptiveComment #11
andypostbtw maybe better to add new "factory method" to existing database class instead of new class?
Comment #12
longwave@tstoeckler Shouldn't we provide the factory so custom code can connect to arbitrary databases, not just default and replica?
Comment #13
tstoeckler@longwave On the one hand, I think if people are using additional database targets they could also provide respective services for those. On the other hand, I wouldn't necessarily be opposed to providing a dedicated factory class+service, but in that case, wouldn't it make sense to deprecate
Database::getConnection()and actually move its contents intoDatabaseFactory::get()? That way we could actually use the newdatabase.factoryservice as the factory key for thedatabaseservice declaration.Just thinking out loud here, I don't mind if we proceed as is...
Comment #14
longwaveFixed points from #10 and some suggestions from #13. Deprecating
Database::getConnection()looks tricky as it needs many of the parts encapsulated elsewhere in the class. Maybe we could just remove theabstractand makeDatabaseitself the factory, although renaming it would be nice,Comment #16
longwaveFixed typo.
Comment #17
longwaveWhoops, bad patch. Interdiff is ok though.
Comment #19
mondrakeLooks nice!
Since we're touching this, shall we use the ClassName::class notation here? i.e. DatabaseFactory::class instead of 'Drupal\Core\Database\DatabaseFactory' and others involved too.
Can we unify on a naming convention for the variable? $database_factory or $connection_factory?
I think the new class deserves a test of its own.
Comment #20
longwaveFixed #19 and added some basic tests. Not sure what else we can add without duplicating ConnectionTest.
Comment #21
longwaveOne more tiny docs fix.
Comment #22
mondrakeMaybe we can also test that $connection is the same as what you get via
Database::getConnection()and via\Drupal::database()?Comment #23
mondrakeRerolled, used the new service in the temp conversions made in #2873684: Replace all calls to db_select, which is deprecated, addressed #22.
Comment #24
berdirWe already tried to properly container-ize the database code a very long time ago in #1811730: Refactor Database to rely on the DIC solely.
That was closed as outdated, but that's the wrong status, the reality is more like "Closed (we give up, can't work)" ;)
This is I guess a small step in making it a bit better, but making the factory itself a service and not just a static method called by the container is most likely not going to happen, ever, as we need the database to be able to initialize the container, also fun during the installer.
Comment #25
longwave@Berdir: Yeah, I entirely agree that we can't move this entirely into the container because of the chicken-and-egg problem around bootstrap and install. However this fix (or at least something like this) is needed to get an alternative connection injected for classes that need this. The other suggested option was to hardcode the @database.replica service and ask users who need further database connections to set up the service definition themselves.
I guess the question is: is there a use case for dynamically selecting an arbitrary database target, in which case we probably need a factory service? If not, we can just provide the replica service in core to solve this issue.
Comment #26
berdirYeah, I mostly just wanted to add some context/background.
isn't this exactly what you are asking for, this does dynamically select default or replicate?
Also, I'm wondering why DatabaseFactory needs to extend from Database? That means you could call all those other methods of Database on it too, which actually seems wrong?
Why not just make it a standalone class that calls Database::getConnection() instead of static::getConnection() ?
Comment #27
longwaveWell, in that case we could just inject both
databaseanddatabase.replicaand decide between the two, so it's not fully dynamic - I was thinking of the case where somehow we get an arbitrary string as a connection target and need to resolve that.Second point is a good idea, let's do that.
Comment #28
tstoecklerIf I understand #27 correctly and the plan is to reduce scope and simply provide a
database.replicaservice in addition to the existingdatabaseservice, then I think that's a good idea. It willComment #29
longwaveSo it turns out we already have a
database.replicaservice, it has existed for several years - it is just buried elsewhere in core.services.yml. This patch moves it next to thedatabaseservice and injects it into the comment and search modules.Comment #30
tstoecklerWow, I didn't know that. That's very bizarre, but I think it's a nice solution anyway, even if it's apparently a sort of "pre-existing" solution. From my point of view this is RTBC if it comes back green, but another review couldn't hurt, so not touching the status just yet.
Comment #31
mondrakeI wonder whether it's OK to add the new argument in between previous ones or we should rather add it at the end of the constructor (with a NULL default). For BC. Same in the other injection in NodeSearch.
Comment #32
berdir+1 to 31, see also my comment/rant last night in #2949018-14: [policy, no patch] Make removal of deprecated usages a feature release blocker.
Comment #33
longwaveNW for #31/32, we can easily provide a sensible fallback here in the default connection.
Comment #34
mondrakeUpdated IS.
Comment #35
mondrakeWe should also address the other conversions that were made in #2873684: Replace all calls to db_select, which is deprecated using Database::getConnection:
Comment #36
longwaveNone of those are suitable for DI, so what's wrong with
Database::getConnection()there?Comment #37
mondrakeCan't we use
\Drupal::service('database.replica')? That would allow swapping the service, which using the static call does not. See (with appropriate adjustments) https://www.drupal.org/node/2993033Comment #38
mondrake(which tells me we should reflect something of this issue in that CR...)
Comment #39
longwaveActually I think SearchQuery can just use
$this->connection.extend()passes its own connection object into the SearchQuery constructor, so it should already be using the replica connection?Comment #40
mondrake#39 sounds right, good catch :)
Comment #41
mondrakeAddressed all points of review.
Comment #42
mondrakeComment #43
longwave#41 looks good to me but not sure I can RTBC as I already worked on a lot of it.
Comment #44
andypostIt looks good to go now, not sure about moving service in core.services but it makes sense to keep databases near
Comment #45
tstoecklerYeah, I actually think the moving of the service is a pretty nice touch. I actually have a project where I defined that service myself in a custom module because I hadn't found it in core. Arguably that may say more about me than about core, but I still think it makes sense in the scope of the issue to "promote" the usage of the service.
Comment #46
longwaveI think it definitely says something about the sprawl of core.services.yml, that this was defined but nobody knew it was there. Should we have a coding standard for the order of services.yml files? Is there anything that makes sense other than alphabetical, as multiple related services are generally namespaced anyway?
Comment #47
catchCommitted cff48a2 and pushed to 8.7.x. Thanks!
Fixed a couple of code style issues on commit.
Comment #49
mondrakeEdited the draft CR https://www.drupal.org/node/2993033, adding the findings here.