Modifying existing table structure - best practices?
I need your advice on how to best deal with the following situation:
Often, plugins enhance the existing Vanilla application by e.g. adding additional data to a discussion.
As far as I know, there are three ways to do this:
- Modify the structure of the Discussion table on plugin setup:
public function Setup() { $Structure = Gdn::Structure(); $Structure->Table('Discussion') ->Column('AdditionalData', int, true, 'index') ->Set(); }
- Add an independent new table with foreign key = DiscussionID
- Make use of the foreignID field in the Discussion table
Now, I have seen and used approach No. 1 myself. It is fast, but if one deactivates and activates the Vanilla application again, all additional plugin data is removed from the table by default.
Approach No. 2 is more robust, but requires an additional database query each time a discussion is loaded (or perhaps a join can be added to the existing query via event?).
Approach No. 3 is equally performant as No. 1, but should probably remain reserved for plugins offering new discussion types such as the poll discussion type (hosted Vanilla).
Is there a 'best practice' solution?
Comments
Put your changes in a public method called Structure. If the class implements the Gdn_IPlugin interface, this method will be called on db structure updates if the plugin is enabled. This should cover the weird use-case of preserving data during Vanilla application deactivation.
If you are really worried about data integrity, use a foreign key in a new table.
The last thing I would mention is to make sure your fields are nullable if at all possible.
Search first
Check out the Documentation! We are always looking for new content and pull requests.
Click on insightful, awesome, and funny reactions to thank community volunteers for their valuable posts.
You can also use the Attributes field on the Discussion table. For data you don't need to query on, that's preferred. Otherwise, I'd prefer columns.
Using ForeignID is dicey because it's typically used to "attach" the discussion to external-to-Vanilla content, not for joining. I don't recommend that approach.
If I had a plugin that would selectively add quite a bit of data to discussions (as in, only some discussions, and lots of data I'd need to query on) I might favor the separate table approach, but probably not outside that use case. That's how we do Polls, for instance, but not Q&A, which uses new columns.
Also want to call out @hgtonight's info as very important. Disabling things in Vanilla should be non-destructive.
Does that mean new columns should accept null values? Wouldn't setting a default value be also okay? If not, why?
Setting a default is also fine, yes, but typically additional columns are optional, meaning they are not set for some records, and the preferred way to represent blank/unset data in Vanilla's database is a null value.
@hgtonight: Are you sure? I couldn't find any such mechanism in the Vanilla 2.2 code. However, it seems there might be a way to preserve the additional columns by overwriting the 'BeforeSet' event of Gdn_DatabaseStructure...
structure()
is indeed a special case. To my knowledge @hgtonight was correct.@mtschirs The magic happens here:
https://github.com/vanilla/vanilla/blob/fd5346/applications/dashboard/controllers/class.utilitycontroller.php#L336-L342
Also note that DatabaseStructure::Set() doesn't remove columns or drop tables by default (unless you pass true to both arguments).
My themes: pure | minusbaseline - My plugins: CSSedit | HTMLedit | InfiniteScroll | BirthdayModule | [all] - PM me about customizations
VanillaSkins.com - Plugins, Themes and Graphics for Vanillaforums OS
hm, the magic you mention seems to be a very theoretical concept. I disabled and enabled the Vanilla application and all data of my 'Discussion Event' plugin was deleted from the Discussion table.
Clicking on the "Enable" button causes a GET request to be send to /dashboard/settings/testaddon/Application/Vanilla/XXXTransientKeyXXX?DeliveryType=VIEW
PHP returns the following error for this request:
As a result, the DiscussionEventDate column has been dropped from the Discussion table. Also, there seems to be an error in Gdn_MySQLStructure::_modify() since it tries to delete the column twice, probably due to the column being an index and Gdn_MySQLStructure::_modify() not handling dropping of indices correctly.
Are you sure? If the first argument 'explicit' is set to true (and it is set to true by almost? all applications and plugins), it calls Gdn_MySQLStructure::_modify() which drops these additional columns.
Confirmed: The flaw lies with Gdn_ApplicationManager::testApplication(): This method does not respect the 'magic'
Yes. It is definitely non-destructive for plugins that do it like in @hgtonight 's example.
I have never disabled the vanilla application though. If that is destructive it should be changed.
My themes: pure | minusbaseline - My plugins: CSSedit | HTMLedit | InfiniteScroll | BirthdayModule | [all] - PM me about customizations
VanillaSkins.com - Plugins, Themes and Graphics for Vanillaforums OS
Just created two github issues for this, the later (https://github.com/vanilla/vanilla/issues/2931) should be fixed quickly since user data is at risk...
Are there currently methods in the DiscussionModel to get and set attributes on discussions, or must the field be retrieved, then unserialized and modified from there?
Add Pages to Vanilla with the Basic Pages app
In Gdn_Model there is
SaveToSerializedColumn
expandAttributes
andcollapseAttributes
getRecordAttribute
andsetRecordAttribute
And
getID
unserializes attributes automatically.My themes: pure | minusbaseline - My plugins: CSSedit | HTMLedit | InfiniteScroll | BirthdayModule | [all] - PM me about customizations
VanillaSkins.com - Plugins, Themes and Graphics for Vanillaforums OS
Look in /library/database/class.databasestructure.php:
As you can see here, set does not allow to drop a table without recreating it.
The model interacts with the database but in Vanilla it doesn't change the structure of tables. This is done with Gdn::structure().
The structure methods have similar names like the corresponding sql commands. Deleting a table in sql is done by
DROP TABLE TableName
. In Vanilla you would needGdn::structure()->table('TableName')->drop();
This is misleading. The function is not implemented in databasestructure but in mysqlstructure which extends databasestructure. That means, if you would like to support another db, you would have to implement the drop() function, otherwise you would see the error message you found in databasestructure...
So "Gdn::structure()->table('TableName')->drop();" will drop the table and then recreate it???If so, not useful in the OnDisable hook.
Nope, but you also asked about the parameters of
set()
This will add a column to the discussion table:
This will remove every column in Discussion table and add the column:
This will drop/delete the Discussion table and then recreate it with only what is specified below:
But all those
set()
examples have in common, that at the end of the call there is still a table "Discussion".Gdn::structure()->table('TableName')->drop();
will kill your table once and for all ("Discussion" wouldn't be wise )