You are not logged in.
For Firebird it seems to be necessary. But perhaps Zeos has a problem. When I look at infos for Firebird, they say deadlocks are extremly rarely, but in my case this deadlock error comes without the "new" Timeout-Option in 100%.
@danielkuettner,
Please, see http://synopse.info/forum/viewtopic.php … 858#p13858. "Deadlocks" is not the most suitable word, "conflicts" is and IMHO they will happen quite often in Firebird as in your case.
I think the Firebird case worth further investigations, since using it improperly may cripple the otherwise fine MVCC idea with mORMot.
As I said, no idea what happens with tiNone isolation in ZEOS. But using multiple connections/threads will make conflicts very easy in MVCC. Even for things which are quite natural in RDBMS with locking.
With my humble experience with Firebird, I want to point your attention to some earlier posts:
In short:
- mORMot.pas by default, lock all write access to the DB with a critical section;
- SynDBZeos.pas does create one connection to the DB per thread.
Perhaps the multi-thread abilities of Firebird client has some problems to scale at the mORMot pace.
and:
Firebird transactions work a little different than in other DBs. Firebird uses mvcc (Multi Version concurrency Control). To those not familar with this interesting concept, it is worth reading about it.
To make a Long Story short, you should try to change the transaction Isolation Level from the Firebird default of snapshot to readcommited.
So martin.suer noted that Firebird uses MVCC for concurrency control, not locking as other DBs. The word "Deadlock" from the error message should be read as "Conflict" in that context. As long as mORMot creates different connections for each thread, those conflicts will be inevitable.
On the other hand, for Firebird there is no such thing as DML outside a transaction, so I can't guess the effect of Zeos tiNone transaction isolation used in:
constructor TSQLDBZEOSConnection.Create(aProperties: TSQLDBConnectionProperties);
begin
inherited Create(aProperties);
...
fDatabase.SetTransactionIsolation(tiNone);
end;Perhaps that is the reason for:
AcquireExecutionMode[execORMWrite] := amBackgroundThread;
AcquireExecutionMode[execORMGet] := amBackgroundThread;
were the options, where no updates were visible more. I've successful tested with not set these options.
In the background process all DMLs are executed properly, but there is a need for an explicit Commit at all clients in order to see the changes if isolation is tiReadCommited or higher.
I guess that playing with TSQLDBConnectionProperties.ConnectionTimeOutMinutes just triggers some implicit Commit and that is the reason everything to look fine at the surface. I suggest explicit StartTransaction/Commit to be used at the Client instead of changing the execution mode and the connection timeout property.
AFAIK the Oracle also uses MVCC and since MSSQL 2008 there is an option to run the server in either locking or MVCC mode. So this can be an issue on other RDBMSes too.
Regards,
Perhaps the affinity of the coalesce() is a little bit fuzzy ![]()
Will it work with all DBs?
I guess we should use "" for Sqlite3, Oracle and PostgreSQL, [] for MSSQL/Access, `` for MySQL.
All of them support double quotes. Not sure only for MS Access, but honestly, I don't wanna know ![]()
What's wrong with the case sensitivity?
So perhaps putting the underscore at the end of the name would be preferred?
Sounds reasonable.
Use MapAutoKeywordsFields option.
The functionality is incorrect, because underscore can't be a start of a SQL-92 identifier. See: http://www.contrib.andrew.cmu.edu/~shad … ql1992.txt, section 5.4, and this applies more to the general case than to the example 01.
MapField is more appropriate for the case, but IMHO the delimited identifiers are the standard way.
See also:
https://docs.oracle.com/database/121/SQ … SQLRF00223, section 6
http://www.postgresql.org/docs/9.3/stat … xical.html, section 4.1.1
http://technet.microsoft.com/en-us/libr … l.80).aspx
"01 - In Memory ORM" example fails when modified to work with ZEOS+Firebird2.5.
The exception is:
Project Project01.exe raised exception class EZSQLException with message 'SQL Error: Dynamic SQL Error SQL error code = -104 Token unknown - line 1, column 59 Time. Error Code: -104. Invalid token The SQL: CREATE TABLE SampleRecord (ID BIGINT NOT NULL PRIMARY KEY,Time BIGINT,Name BLOB SUB_TYPE 1 SEGMENT SIZE 2000 CHARACTER SET UTF8,Question VARCHAR(200) CHARACTER SET UTF8); '. Process stopped. Use Step or Run to continue.
AFAIK, the tokens TIME,DATE are reserved words in Firebird along with the DATETIME. Perhaps adding a double quotes around the column names will resolve the problem.
Regards,
I've found this function for generating the IDs (shouldn't it be Int64?):
function TSQLRestStorageExternal.EngineLockedNextID: Integer; {virtual;}Please, notice that fEngineLockedLastID is assigned a value in 3 more places. In the following method it is assumed that the generated ID values will be adjacent, which may not be the case and strongly depends on the EngineLockedNextID implementation:
procedure TSQLRestStorageExternal.InternalBatchStop;
...
finally
if (fBatchMethod=mPost) and (fBatchCount>1) then
// -1 since fBatchFirstAddedID := EngineLockedNextID did already a +1
inc(fEngineLockedLastID,fBatchCount-1);
...Two questions:
Isn't it appropriate to introduce the method EngineLockedNextID earlier in TSQLRest in order to create a seam for a
customizable ID generator?
Can we have a dedicated interface (abstract class) for such a customizable generator?
Regards,
@ab,
Since I'm not sure how I can be helpful, I'll remain available if you have more questions. Meanwhile, I'll try to get more familiar with the framework. Thanks!
What do you mean exactly by "Was the mORMot uses RDBMS DRI peculiarities to support the persistence?"? (there should be a missing word)
Quite possible! Sorry, English is not my native language.
What I meant was that, I'll be surprised to see mORMot trying to use other than just simple DML expressions by several reasons:
Multiple DB back-ends, each with its own language (and semantic) peculiarities;
Entanglement with a patterns considered as not suitable;
Introduction of a hidden behavior, which cannot be followed at a higher level;
Usage of NoSQL engines lacking most of the properties of the classic RDBMS;
My confusion is about implementing the same features (e.g. FK constraints) on a higher level which is IMHO following the same patterns.
Newbies confused! ON DELETE SET DEFAULT is a FK constraint, not trigger. Was the mORMot uses RDBMS DRI peculiarities to support the persistence? Isn't it all about to go beyond the RDB patterns?
How do you make the purge of the journal?
For the externally imported changes it suffice to leave just the rows with max(donorkey) for each <tablename, donorsid> pair. For the local changes purging must be done according to the most lagging external site, but again, one row must be left for each <tablename, donorsid=localsid>. Now, the assumption is that no one will lag more than 30 days, since no information is recorded about the lag.
If I understand correctly, any issue due to potential clock de-synchronization is fixed by the order on which you apply the journal events?
It should be.
Since the site id is part of the record id, why do you maintain separate donorid/siteid columns? Only for performance reasons?
That was in case I decide to change the scheme of generating keys (set @out_key = @key * 10000 + @sid); Besides, I was not quite sure how the arithmetic will impact on the index processing.
I wanted everything to work in the first version, and as we all know "Premature optimization is the root of all evil"
. So I stuck just to the bare minimum. Never optimized it since then.
BTW when I got rid of the MS subscriptions/publications, I got so much free room that I decided even not to normalize the journal (tablename). Actually there is lot to be improved, e.g. the local row deletion makes obsolete all changes made before to the same row (but may impact DRI), etc. but, do I really need it?
More details on the journals...
A small excerpt from the journal:
key siteid tablename recordkey op stamp donorkey donorsid
------------- ------- ---------- ------------- -- ----------------------- ------------ --------
52424390017 17 Subscript 52424380017 I 2014-01-27 10:32:33.187 52424390017 17
52424470017 17 Subscript 52424380017 U 2014-01-27 10:32:37.497 52424470017 17
52424490017 17 Subscript 52424480017 I 2014-01-27 10:32:43.420 52424490017 17
52424530017 17 Subscript 52424480017 U 2014-01-27 10:32:49.280 52424530017 17Here key is the PK of the table, siteid is the identifier of the site where the CRUD operation occurred, tablename, recordkey are the row address where occured, stamp is the timestamp, donorkey is the key of the foreign journal table from where the change was imported, donorsid is the identifier of the site from where it was imported.
(each site has unique small number for site id, 'donor' is the foreign site)
On local updates donorkey equals key, donorsid equals siteid.
And the sync goes like this:
// check in our journal for last update from that site/table
SQL.Add( 'select coalesce(max(jlocal.donorkey),0)' );
SQL.Add( 'from journal jlocal ' );
SQL.Add( 'where donorsid = :dsid and tablename = ''' + tableName + '''' );
Params[0].Value := donorSiteID;
Open;
lastEntry := Fields[0].Value;That is for retrieving the last update applied from the specified 'donor' for a given table.
And then:
SQL.Add( Format( 'select top %d', [recordsLimit]));
SQL.Add( ' j.key as "$$key", j.siteid as "$$siteid",');
SQL.Add( ' j.tablename as "$$table", j.recordkey as "$$rkey",');
SQL.Add( ' j.operation as "$$oper", j.stamp as "$$stamp",');
SQL.Add( ' j.donorkey as "$$donorkey", j.donorsid as "$$donorsid",');
// Here are the fields from the data row with the original field names
SQL.Add( ' l.*');
SQL.Add( 'from journal j left join "' + tableName + '" l on');
SQL.Add( ' j.recordkey = l.key');
SQL.Add( 'where');
SQL.Add( ' j.key > :lastentry and' ); // Newer journal entries
SQL.Add( ' j.donorsid <> :lsid and' ); // Not originating from here!
SQL.Add( ' j.tablename=''' + tableName + ''' and' ); // For requested table
SQL.Add( ' (j.operation = ''D'' or l.key is not null)');
// The keys are increasing so the order will be chronological
SQL.Add( 'order by j.key');
Params[0].Value := lastEntry;
Params[1].Value := localSiteID;
Open;Now we have a dataset with the rows inserted, updated or deleted in the foreign table since the last sync. The reason to include 'D' operations is to process them in chronological order.
Next:
while not EOF and (recordsLimit > 0) do
begin
case Operation of
'I': InsertRec;
'U': UpdateRec;
'D': DeleteRec;
'R': CheckInRec; // Insert or update?
else
raise EDatabaseError.Create('Unknown operation!');
end;
...
// Copy the foreign journal entry just processed into the local journal
InsertJournalEntry;
...The InsertJournalEntry goes like this:
procedure InsertJournalEntry;
begin
with qryCloning do
begin
SQL.Text := 'exec dbclone_insert_in_journal :key, :siteid, :table, :rkey, :oper, :stamp, :donorsid';
Params[0].Value := qryDonor.FieldByName('$$key'); // This value will be written in the donorkey column!
Params[1].Value := qryDonor.FieldByName('$$siteid');
Params[2].Value := qryDonor.FieldByName('$$table');
Params[3].Value := qryDonor.FieldByName('$$rkey');
Params[4].Value := qryDonor.FieldByName('$$oper');
Params[5].Value := qryDonor.FieldByName('$$stamp');
Params[6].Value := donorSiteID;
ExecSQL;
end;
end;The only trick is the usage of donorkey and donorsid values. Used that way there is no need for a separate table for keeping information about each remote site and the corresponding sync progress. Also, it keeps track for the changes propagation among the databases.
Hope no one got bored!
Regards,
@EMartin
Thanks for the pointer, I wasn't aware of the product, it looks feature-rich ... and claims to have a lot of advanced functions.
I believe that mORMot have it's internal handling of the schema changes and may be the task of schema syncing is a lot simpler compared to a classic RDBMS. Mssr. Bouchez could express an opinion on that.
@ab
I was engaged several months in implementing the replica in T-SQL+FPC. Nevertheless, I want to note why mORMot way should be easier. BTW, much or less as you said:
We have already in the framework a lot of components to implement it.
We implemented history tracking in a few code lines, some weeks ago - see http://synopse.info/files/html/Synopse% … ml#TITL_85
The REST transport layer is just perfect for the purpose. Previously restricted at TDS with no options;
The tracking can be implemented in Pascal, no need for T-SQL/P-SQL/x-SQL triggers and hacks for detecting replication sessions or filling default values on OID columns;
Engaging the change-sets into the memory/cache can greatly reduce the row/table locks and thus improve performance;
It will be very easy to find the table dependencies via RTTI and make the right master-detail tables sorting instantly. Now the order is hard-coded and it is computed (tool) from the DB metadata during the planning stage;
The OID's can be computed without a DB round-trip for each row.
Regarding the 4).
2.1. before Delphi 2010: we specify it as a specific type so that the table would be retrieved from the type name (TSQLRecordClientID -> TSQLRecordClient)
type TSQLRecordClientID = type(TID);
...
published OrderedBy: TSQLRecordClientID read fOrderedBy write fOrderedBy;
I think that should be the natural way.
Regarding the 5).
Consider my T-SQL OID generator:
CREATE procedure dbo.dbclone_genkey
@out_key bigint output,
@in_increment bigint = 1
as
begin
declare @sid bigint, @key bigint
set transaction isolation level repeatable read
set xact_abort on
begin transaction
select top 1 @sid = dbclone_id, @key = dbclone_genval
from dbclone_siteid order by dbclone_ord
update dbclone_siteid
set dbclone_genval = dbclone_genval + @in_increment
where dbclone_id = @sid
commit transaction
set @out_key = @key * 10000 + @sid;
endHere we have @sid for a DB site identifier and @key for the next key to be returned, which must be modified and written back. This is our bottleneck. All CRU's must go through that procedure. The solution is to pre-allocate a range of ID's with the @in_increment parameter and to have just one round-trip for a block. This is not possible in pure T-SQL, but absolutely feasible at the mORMot OPF level.
I'll continue to feed you with the details, at least until you say it is enough
.
And WOW! You're lightning fast with the changes!
@mingda
Thanks for sharing your thoughts! It is worth to note that the merge replication is a vast topic for discussion and it is a matter of benefits and trade-offs what decisions the developer will made in each concrete situation.
Your considerations are about the "granularity" of the journal, the approaches may be for "row" or "column" values. In my previous post I expressed some doubt about the aggregates and that is about the same thing. I'll try to explain:
if TSQLRecord have old value, use the old value generate upate sql,
userA update will generate
update Test set column1 = 'value_updateA' where id = 1 and column1 = 'value',
userB update will generate
update Test set column2 = 'value_updateB' where id = 1 and column2 = 'value',
this solve two problem, 1) update override other column's value 2) multi user update conflict.
Irrelevant, because each user works in it own DB, and what you describe is more subject to transaction isolation (if you suspect something will change between select/update) than the SQL logic. Furthermore, you can't rely on other fields to select because they are not immutable as OID's are.
if we have old value, such journal will look like such:
1). {"ID":1,"column1":"value","column2":"value","column3":"value"}
2). {"old record":{"ID":1,"column1":"value","column2":"value","column3":"value"},
"new record":{{"ID":1,"column1":"value_updateA","column2":"value","column3":"value"}}}
3).{"old record":{"ID":1,"column1":"value","column2":"value","column3":"value"},
"new record":{{"ID":1,"column1":"value","column2":"value_updateB","column3":"value"}}}
Here you want to introduce the "column" value granularity in a some redundant way, generally, the finer granularity is, the less probability for conflict exists. But it is a bit more complicated for the implementation. May consider the YAGNI principle ...
In our legacy system, there are inherent ownership of the different tables, so we decided that the row granularity is perfectly enough for the purpose.
Another decision that we made was that the conflict resolution will be in favor of the node which started the synchronization (assuming it is on a higher administrative level), i.e. the synchronization direction defines the conflict winner.
According to many papers on the Web, there is no such thing as "Conflict-free replication" in general, so the conflicts are inevitable and we must find a way to live with them. It is a matter of clearly defined conflict resolution discipline.
Regards,
Two doubts:
I don't think the timestamps are good, imagine nodes in different timezones, DST transitions, somebody changed the date, etc. Better is to rely on the monotony of the ID generator;
Into the RDB design and when normalizing, the number of tables tends to increase. This makes the change "impact" area smaller and gives less probability for conflicts. When de-normalizing, the entire aggregate will be contained in a single row and a slightest change will induce conflict; Imagine a proforma invoice with two rows, in a classic RDB the rows will be in a secondary table, when different rows are modified on different nodes then everythng will be ok, but when denormalized, the rows will be contained into the proforma and modifying any of the rows will mark the entire document as modified;
Personally, I don't like the attributes at all. I have a long experience with the .NET Frameworks (by necessity) and my opinion is that when somebody have no idea how to express something then he "invents" the attributes.
moctes wrote:About sortable unique ID's there is this blog post from Rob Conery which shows a way to solve it, although is implemented as a PostgreSQL function and DB Schemas maybe you could borrow some concepts ?
This is in fact pretty close to what MongoDB expect.
See for instance how we generate an ID on client side in our SynMongoDB.pas unit, in TBSONObjectID.ComputeNew.
In fact, there are different schemes for generating ID's, Hi-Lo, LB Alloc, etc. As long as they are globally unique, they should do the work. Even the UUID/GUID's!
If your point was about replacing ID/RowID from integer to another kind (e.g. TGUID or string), it won't be possible directly, due to the design of SQLIte3 virtual tables (which we use in our ORM core).
@ab I am browsing through the sources, but I can't find where the that dependency exists (besides pointer/Integer thing). As long as I can see, the RowID's in SQLite3 are Int64. Even the https://www.sqlite.org/c3ref/module.html defines the pRowid in (*xRowid) is an int64. Of course, the source is quite big, but I'll appreciate if you can pinpoint me some line where this can be seen.
Regards,
OK, got it!
Long time ago, when I was a student, someone said "The one who undertakes a merge replication is just looking for trouble...". So we are ![]()
What about the amount of data?
Total data, in KB or rows? Temporary data on each node during off line mode?
Could you not split the data into smaller table, reducing the scope of each data? (i.e. implementing some "ownership" of data, making some part of the data R/W for one node, then R/O for other nodes).
What is your expected delay for synchronization around nodes, for the data to be eventually consistent?
Do you need ACID behavior, if yes, at which level?
How does it resolve the conflicts? Do you use a version number for the system?
Approx. numbers as I recall: total number of rows: 6M+, synchronization performed on 3-8 min. with mutual prime number of minutes for different nodes, about 100-150 rows max on a single synchronization, data ownership is implicitly defined by the application itself (three levels are actually different in their functionality). ACID is not expected at 100% but every violation must have a clear manifestation. Conflicts are resolved by preselection who will be the winner from the two participants.
For the feature request: http://synopse.info/fossil/tktview?name=3453f314d9
I think the simplest way to implement such a master/slave synchronization is to include a table-wide version number column, VN, no need for TModTime,TCreateTime. Every insert initializes the column in the inserted row with the max(VN)+1, every update also updates it into the updated row to max(VN)+1. The rows for the next synchronization then can be simply selected from the master as those with VN greater than the max(VN) from the slave table. The trouble is the deletion, but it can be either resolved by marking rows as deleted rather than actually deleting'em (and setting VN to max(VN)+1) or making a side "tombstone" table with the PK's of the deleted rows. It is not a big trouble, as long a surrogate (not a natural PK) is used which is guaranteed to be "stable".
The peer replication case:
It is crucial to have a way to uniquely address a row across multiple databases, so a unique value (among all nodes) must be assigned for each inserted row. It may not be the PK, but if the PK is auto-increment value, it may happen that the same value is already used in the other DB and that will lead to inevitable conflict. So, it will be best if we can use that "address" column as a PK also. Furthermore, for the reasons mentioned before, it will be also good for the generated unique values to have additional properties, e.g. monotony.
In a true RDBMS case, each CRUD operation is registered through a corresponding trigger. The registration is made in a side table, lets call it "journal" table. Each row is consisting of: The name (or some id) of the table, the type of the operation (Insert, Update, Delete), the unique "address" of the affected row, and finally the originating node id of the operation. The last thing is for distinguishing the local changes from external ones.
The synchronization goes as follows: for each synchronized table (the order is important, more on that later) the greatest "unique address" of external change is taken and rows with the greater value extracted from the other side journal. They should describe the latest changes from the other side. These rows left-joined with the corresponding table gives the complete data set to be INSERT OR UPDATEd, deletes have NULLs in their data columns. Of course this is the case when the unique "addresses" are ordered in the time domain, otherwise the comparison must be made on an additional timestamp field. The same thing must be repeated backwards to complete the table synchronization. It is also important to transfer the foreign journal entries into the local journal to mark the changes as "already applied".
Some difficulties exists, one of them is the FK enforced DRI. By the way, during the replication MSSQL disengages most of the constraints and triggers with NOT FOR REPLICATION directive. Our solution is to synchronize the tables in strict topological order, with master tables first, then the slave tables. This will work when no loops exist into the FK constraints, also will work for a simple loop into the same table since the modifications are timely ordered. Will not work when longer loops exist, but I haven't seen so far such an "exotic" DB schema. There is a tool developed which extracts the dependencies between the tables and does the topological sorting.
Other difficulty is the initial snapshot of already operational DB, the simplest way to do this is to perform a backup and then restore it on the other node. Alternative approach is to define additional operation in addition to the I,U,D, e.g. Register and to perform that operation on all rows in the entire database. On the other side all such operations will be treated as INSERT OR UPDATE and thus (taking much mooore time) the entire content will be transferred. Of course, on an unprepared DB the row insertion order is unknown and the circular dependencies can be a real problem. The advantage of the latter approach is that the source DB doesn't go offline.
But after such a prolonged writing, the introduction of the "shared nothing" principle discards all the concerns mentioned above ![]()
Regards,
Actually, I can supply you with all the details about our solution, just feeling uneasy to bother someone with all particularities. Also still I'm not sure how that can apply to mORMot and will it be of real value for the project. But if there is a real interest of that aspect (i.e. multi-master replicas), I would like to know ![]()
It will take me some time to browse the links, but I'll try to give some quick explanation. I'll presume that the requirement of global unique identifiers is beyond doubt when it comes to multi-master replication (right?).
What was wrong with the MS solution? (any feedback is welcome)
All DB engines use some data structures at the file level. MSSQL, Firebird, SQLite stores the tables in B-trees. MSSQL calls it "Clustered index". When using GUID as a PK, the B-tree pages are organised according the GUID values which are not monotonic. Usually, most of the tables follow some temporal dependence. Log tables are the most obvious example - they are always appended at the end (with regard of time). Besides that, usually the log tables doesn't have other PK than surrogates. It finally happens that your biggest table have the worst structure which dramatically reduces the data throughput. Even more annoying is that the log tables are usually queried for a specified period of time - using secondary index actually makes the server to engage most of the file pages in memory, because the rows were physically spread across much more pages than they need to. That turns a simple report into a nightmare. They later introduced a monotonic NEWSEQUENTIALID() to overcome this deficiency. See Good Page Splits and Sequential GUID Key Generation.
The lack of the monotonic property is the main trouble with the GUIDs.
2) GUID clustered index keys cause new rows to be uniformly distributed across the clustered index, causing expensive page splits, poor cache performance and about 30% unused space on every database page.
Another problem
1) They are large (16 bytes), and enlarge all non-clustered indexes.
Further
Another possibility may be to rely on the replication abilities at storage level.
At this point I'm concerned about the mORMot multiple caches and the performance penalties imposed invalidating them. It will be best if the replication system is coherent with the caches.
PostgreSQL replication was something in incubation at the time we started the implementation, I'll return back to reading to see the current status.
The function TSQLRestStorageMongoDB.EngineNextID should be overridden to compute an unique per-node ID, either by adding a small bit shift for each node, or by pre-allocating ID ranges for each node.
I'll investigate this suggestion, but not quite sure that Int32 will suffice for my needs. If we assume that nodes will be numbered 0-127, that makes 7 bits and leaves just 24 (signed) for the increasing part. 16M rows can be easily achieved taking in mind that the generator is the same for all tables.
Some additional questions:
- How many nodes do you have?
- How many replicated data do you expect to store?
One working implementation is with 2 levels of tree-like filtered replication; one central node, 5 middle-level nodes and 12 leaf nodes, total of 18. The central node collects the events from all leaf nodes and there the number of rows goes critical. Of course all that is subject to revision/redesign.
Regards,
Thank you for the quick reply!
And my apologies for being not so familiar with the details of SQLite3, mORMot, Object Pascal and such technicalities... My suggestion was is it possible to encapsulate something different than Int32 into, and use it as a GUID (in the general sense, not MS).
May be the Lazy<> is what I have in mind with my limited knowledge about the Delphi generics (I'm from C++ world), and if it offers enough functionality to replace the RowID's in a transparent way - then it is okay.
Just to note a few more things about the system I'm going to redesign - the necessity for 24/7 operation forced us to implement a multi-master replication scheme, it is modeled in a similar principles as the MS proprietary one. In the time of development we faced the problem of the PK global uniqueness. The original (MS) GUID solution just sucks (that is another topic).
Nevertheless, having more than one DB (for writing) imposes the usage of surrogate generation scheme which guarantees the PK's are globally unique. The mORMot OID's are not, and I am just wondering how can I implement such a functionality. May be there is another way to achieve this and I'm simply too ignorant to know it.
Hi there!
First to say that I'm reading the SAD for a couple of days and I'm really impressed by the framework, praises!
Coming from the C++/RDBMS world, I have to go through with the painful paradigm shift (and the induced impedance
)!
We have here a system which I have intention to redesign, It has saturated to a margin that it is hardly maintainable, it is RAD, it is rigid, it is whatever cited as 'bad' of the traditional Delphi/CBuilder design. But it have a multi-master replication, one aspect which eludes me into the SAD. This asset of the existing system is quite useful and valuable.
Anyway, here is my observations, they're about the surrogate keys used in the mORMot:
mORMot object ID's are Int32, they are actually used as PK into the underlying SQLite tables, actually they are unique just for the given table/class.
I perfectly understand why object ID's are Int32, that way they're the same size as pointers and some can painless do MyObj := TMyObj(MyDataBase.Add(One,True)) and vice-versa.
Such a surrogates introduced a locality, which is not easy to overcome when dealing with multiple databases.
Most persistence papers introduced generation of surrogate keys according to some scheme for global uniqueness. mORMot uses auto-id's (correct me if I'm wrong).
Anyway, the TSQLRecord published properties may contain different values written in the same memory cell (see 2). They can be integers or references and it is up to the programmer to handle it in a proper way:
TSQLRecord published properties do not contain an instance of the TSQLRecord class. They will instead contain pointer(RowID), and will be stored as an INTEGER in the database. So the main rule is to never use directly such published properties, as if they were regular class instance: otherwise you'll have an unexpected access violation error.
and later:
When accessing the detail objects, you should not access directly to FirstOne or SecondOne properties (there are not class instances, but integer IDs), then use instead the TSQLRecord. Create(aClient: TSQLRest; aPublishedRecord: TSQLRecord: ForUpdate: boolean=false) overloaded constructor, as such:
And finally, my real question is:
Isn't it worth to introduce a special class for the object ID? Or to encapsulate "smart-pointer" like semantics into the TSQLRecord? I think this will be entirely in the spirit of the OOP and will completely overcome the Int32 limitation.
Best regards,
Yuliyan