Sidebar: I've written this in about an hour, most of the thoughts have been based on approaches I've looked at across the past few weeks, these thoughts are not fleshed out, do contain errors, and probably misrepresent some things that it shouldn't.
Deactive is a thought process behind a D implementation of ActiveRecord?. ActiveRecord? is Fowler's description of modeling around databases, and has several implementations in various languages most notable is probably the RubyonRails? implementation, though it's certainly not the only one.
DeActive should try and use D's features as much as possible. There is nothing gained with copycatting the *implementation* (read: not concepts) of other implementations. DeActive should feel like D, because it *is* D.
Concepts gotten right by the mainstream implementations:
- conventions are not a bad thing
- over configuration is
- conventions with smart defaults are great
- adaptable conventions aren't magic, they're necessary
Case studies we need to consider when developing DeActive
- Bob is starting a new application and needs DeActive to control his model logic.
- Anne has an existing application she's wanting to move over to D, thus her schema is already defined.
- Charlie has already had an implementation in DeActive, but needs to change his database schema.
- Greg has an GUI application that needs a database based backend.
- More?
Using the database to get the needed information, and with conventions modeling against it
In this context we need to ignore Anne's usercase for a minute, I'll come back to it later.
Every database implementation that I know of, and possibly some I don't allows you to get the information about the database and tables it consists of. It also allows you to get the columns, the types, basically.. the entire information you would need to develop a dedicated ORM.
Using that information we can generate the code to map D to the database in a very direct, and efficient manner.
Generated Code isn't a bad thing
Generated code can help along the way as long as it can handle changes to the code that was generated. This is possible in D, with a few considerations.
We generate a struct based on the table layout, determining the types on the columns and the names during the generation cycle.
- There should be conventions applied to column naming to allow specialized types to occur.
- Having each row linked to a struct should allow us to keep performance optimal, while allowing representation to occur at a readable level.
The conventions should provide smart defaults, with an option to override for Anne's usercase mentioned above (the database is already defined, so changing it to suit DeActive would be a silly, and costly cause)
- There is a need to convert names in a way that is legal in D, so spaces would have to be converted to underlines.
- An example would be a datetime column named created_on. This column would have a datetime inserted on the insert of the object.
Each generation cycle would only change the structs that map to the tables, as *they should*. If there is a change into the database schema it needs to be represented into code. There shouldn't be any reason to implement anything inside the structs, they are strictly a throw away type.
Module Naming
A quick thought on module naming on all generated code would go as the following:
"user defined prefix".databasename.tablename;
You could also generate code that allows importing all of DeActive for a defined database through
"user defined prefix".databasename.all;
Possible to define imports for associated tables (in a similar manner to all)
Types
It's worth noting that you can, within limits have one to one types with most of d's built in types. But it wouldn't be optimal. A char[] could technically map to a varchar field, but in doing so wouldn't have the length limitation, or the default limitation defined. we could use a struct in place called varchar that could handle the workload.
varchar(length,"default");
Other types could be handed in a similar manner.
The Factory class
Each table gets a factory class generated. This is where the custom code is implemented. The factory class handles all interaction to the database. Most of the code can be implemented into a mixin.
class TableNameFactory // better naming scheme? { mixin Model!(TableStruct); // all overriding code here S validate(inout S s) { //domain specific validation } }
Why is this significant? For a couple of reasons. The generated code doesn't get into the way of domain specific logic. You can override implementations without a deep class hierarchy.
template Model(S) { static S find(int a) //etc static S[] find(char[] sql) //etc static void save(S s); pre_validate(s); validate(s); if (is_valid(s)) // perform save; static void save(S ...) static bool exists(S s); //etc // call backs! S pre_validate(inout S s) S pre_save(S s) S after_save(S s) S after_delete(S s) bool is_valid( S s) S validate(inout S s) }
Note: unimplemented, untested, written on the fly code!
Associations
Associations can be preformed with structs as well, generated code would be able to generate the associations on the fly
// one to one struct Person { Address address; char[] name; } struct Address { char[] street; char[] state; //... } //one to many struct Person { Address[] addresses; char[] name; } struct Address { char[] street; char[] state; //... }
Anne's user case
When using conventions, or associations we need to keep in mind pre-existing database setups. Here's a proposal on that aspect.
When reading the database and generating code we can generate a dsl of sorts in pure D. Not some parsed syntax, but pure D.
Model ( Database ( "name.db", Tables [ Table ( "name", Columns [ Column (Type.VarChar, 255, "default"), //etc ], ) ] ), Associations [ // syntax for linking assoc. // syntax for defining custom types (created on) ] )
This isn't exactly going to work as it would be best to align associations and custom types within the table declaration, but it gives you an idea on how we can give a readable format for DSL information within D, without creating a dsl that we would have to encode, decode outside of D's semantics.
Given this, Anne could run the generator script against the database and then fine tune the associations, custom types without having to change the database itself, the generator could then run this file to deliver the set of code.
Misc
The database should be agnostic. There should be no reason to name the database directly in the code. Passing a connection struct that contains, host, database name, username, password and database type should be suffice. Let DeActive do the dirty work, not the users.
Given the above we could deliver a source code directory, a build script and even documentation (uml graphs?) I mean it's really endless.