Files:
The Data Manager example shows how the user can make use of the provided default query classes for loading data sources to the ListView. The difference between using these and writing your own DataModel is that they make use of a cache manager in combination with integrated options which improve the speed and efficiency of the query results. This in turn allows for the smooth scrolling of the ListView when dealing with large amounts of data.
In this example we will learn how to use the default classes to display data that is sourced from a large data set, which could cause display lag if used inproperly. These classes are part of the core datamanager library, but have been provided with this sample to allow you a glimpse of its guts in order to allow you to do your own modifications/improvements to them. Learning these classes will allow you to build upon these defaults to provide your own utility classes for other data sources leveraging the library functionality such as DataQuery and AsyncDataModel for it's background caching mechanism.
The UI of this sample application consists of a ListView that simply shows the content based on which data model is in use.
The following global properties define the sql queries that will be used throughout the sample demo to demonstrate the attached objects behaviors.
// global property to indicate when live revision update is in progress // so that a concurrent selection of live updates terminates the process property bool liveUpdate: false; // sql query for updating the revision id in the revision table property string updateRevision: "update revision set revision_id = revision_id + 1" // sql query to select revision id from the revision table property string selectRevision: "select revision_id from revision" // sql query to insert a new artist row. This query makes use of bound properties and up-to-date revision id through select statement. property string insertQuery: "insert into artist(revision_id,name,realname,profile,data_quality,primary_image,is_group,master_count,group_master_count) select revision_id,:name,:realname,:profile,:dataquality,:primaryimage,0,0,0 from revision"
The ListView is used to display the data from a specific data model that has been selected and loaded at that time. It also provides logic for using the QueryExec class in order to dynamically delete a row. This is achieved by setting the QueryExec.queries property with sql queries that select the revision id, which is a required parameter of the emitDataChanged() signal, and a delete query to execute the deletion. In addition, the signal/slot connections have to be established in order to know when to emit the emitDataChanged() signal to force the ListView refresh to reflect your changes on the screen.
Take note that QueryExec was created as a utility class designed to demonstrate ListView behaviour when a background process is modifying the data at the same time the user is diplaying it. Use at your own discretion.
ListView { id: listView // ListView properties in order to be accessible through ListItemComponents property alias deleteq: queryExec property string selectRevision: "select revision_id from revision" // sql query to delete artist with specific name property string deleteQuery: "delete from artist where name = :name" // this variable holds the current SqlDataQuery that the ListView.dataModel is using property variant dq: defaultDataQuery layout: StackListLayout { headerMode: ListHeaderMode.Sticky } layoutProperties: StackLayoutProperties { spaceQuota: 1.0 } horizontalAlignment: HorizontalAlignment.Fill verticalAlignment: VerticalAlignment.Fill // The current ListView data model being used dataModel: defaultModel // Function that returns access path to the data images // _dataDir is a path exposed to the qml context via applicationui.cpp to the apps data folder function imageurl(image) { if ("" == image || !_app.fileExists(image)) { return _dataDir + "../../native/assets/images/no_image.png"; } return _dataDir + "images/" + image; } listItemComponents: [ // ListComponent to represent default dataModel's ListItemComponent { // Displays the row data StandardListItem { id: itemRoot title: ListItemData.name imageSource: itemRoot.ListItem.view.imageurl(ListItemData.primary_image) description: ListItemData.realname // function that emits data changed signal with the revision update in order // to signal for the ListView to refresh; dq holds the dataQuery that the user has selected function onDeleteExecuted(resultData) { var revision = resultData[0].revision_id; console.log("live delete was performed "); itemRoot.ListItem.view.dq.emitDataChanged(revision); itemRoot.ListItem.view.deleteq.executed.disconnect(itemRoot.onDeleteExecuted) } gestureHandlers: [ LongPressHandler { onLongPressed: { // Set the sql queries to retrieve revision and delete the row with the bound name value itemRoot.ListItem.view.deleteq.queries = [ itemRoot.ListItem.view.selectRevision, itemRoot.ListItem.view.deleteQuery ] itemRoot.ListItem.view.deleteq.bindValues = { "name" : ListItemData.name } // Connect to the javascript function, which emits signal to force ListView to refresh after change itemRoot.ListItem.view.deleteq.executed.connect(itemRoot.onDeleteExecuted) itemRoot.ListItem.view.deleteq.execute() } } ] } } ] onDataModelChanged: { console.log("onDataModelChanged....") } onSelectionChanged: { console.log("onSelectionChanged, selected: " + selected) } onActivationChanged: { console.log("onActivationChanged, active: " + active) } }
The first AsyncDataModel is used as the default model at application startup. It's SqlDataQuery only makes use of it's bare bones basic functionality, which is just a select statment to retrieve the data without making use of any optional properties. The result of this is the way the cache manager behaves in this instance. Since we are not assigning any key or revision columns, this causes the manager to replace(reload) the entire cache instance, because it can't know what data has changed - it's only aware that a change occured.
// One of the default provided DataModel's AsyncDataModel { id: defaultModel // Standard data query that does not make use of any performance improving // properties (i.e. keyColumn, revisionColumn) query: SqlDataQuery { id: defaultDataQuery source: _dataDir + "discogs_small.db" query: "select id, name, realname, data_quality, primary_image, is_group, revision_id from artist order by name" countQuery: "select count(*) from artist" onDataChanged: console.log("data changed: revision=" + revision) onError: console.log("SQL query error: " + code + ", " + message) } onLoaded: console.log("initial model data is loaded") },
This AsyncDataModel is used in combination with the SqlDataQuery which makes use of the item-level keys property. If you expect the source data to change, then item-level keys are an important consideration. Item-level keys allow AsyncDataModel to track the location of each item in its cache and to report the movement of items to a ListView, even as items are inserted, removed, or updated in the data source. If item-level keys aren't used, changes in source data may result in jarring visual changes in the ListView.
AsyncDataModel { id: withKeyModel // Data query that uses one of the performance improving properties - keyColumn query: SqlDataQuery { id: kdq source: _dataDir + "discogs_small.db" query: "select id, name, realname, data_quality, primary_image, is_group, revision_id from artist order by name" countQuery: "select count(*) from artist" keyColumn: "id" onDataChanged: console.log("data changed: revision=" + revision) onError: console.log("SQL query error: " + code + ", " + message) } onLoaded: console.log("initial model data is loaded") },
This AsyncDataModel is used in combination with the SqlDataQuery which makes use of the revisions propery. The overall revision allows AsyncDataModel to ensure that different versions of source data are not mixed in the same cache. If different versions of source data are mixed, the same item could appear twice or some items could be missed. Using overall revision is important when data changes are expected in the source data. An overall revision represents the current, unique, state of the data source and is used to recognize changes to the data source. The overall revision must be incremented (or otherwise changed) each time a change occurs in the data source. If overall revisions are not returned, then, to be conservative, each query refreshes the entire cache, resulting in inefficient performance.
AsyncDataModel { id: withRevisionModel // Data query that uses one of the performance improving properties - (revisionColumn, revisionQuery) query: SqlDataQuery { id: rdq source: _dataDir + "discogs_small.db" query: "select id, name, realname, data_quality, primary_image, is_group, revision_id from artist order by name" countQuery: "select count(*) from artist" revisionColumn: "revision_id" revisionQuery: "SELECT revision_id FROM revision" onDataChanged: console.log("data changed: revision=" + revision) onError: console.log("SQL query error: " + code + ", " + message) } onLoaded: console.log("initial model data is loaded") },
This AsyncDataModel is used in combination with the SqlDataQuery which makes use of both the item-level key and the revisions propery. This is mearly to demonstrate the efficiency of using both of the properties simultaneously, and is considered as part best practices when designing your app with large and changing data sets in mind.
AsyncDataModel { id: withKeyAndRevisionModel // Data query that uses both performance improving properties - keyColumn, (revisionColumn, revisionQuery) query: SqlDataQuery { id: krdq source: _dataDir + "discogs_small.db" query: "select id, name, realname, data_quality, primary_image, is_group, revision_id from artist order by name" countQuery: "select count(*) from artist" keyColumn: "id" revisionColumn: "revision_id" revisionQuery: "SELECT revision_id FROM revision" onDataChanged: console.log("data changed: revision=" + revision) onError: console.log("SQL query error: " + code + ", " + message) } onLoaded: console.log("initial model data is loaded") },
The following QueryExec, which is a utility class to facilitate asynchronous data updates, is used to demonstrate data changes while the ListView is displaying the current revision data set. It consists of the sql query to execute on the data, the number of times to execute this query and the delay between executions. It also has a onExecuted signal handler that is initiated after each execution allowing you to perform your own tasks, one of of them being the firing of the emitDataChanged() signal to inform the ListView that the data has changed and a cache refresh is in order.
QueryExec { // *************************************************************************** // Start it by calling execute(). Can be stopped by calling stop(). // Each time it will update x rows (including both overall revision, item revision) // and then notify the data model via its associated query. // Next time (after 1 second delay) it will go down 100 rows and do the same. // // Query sequence: property string updateContact: "update artist set revision_id = (select revision_id from revision) " + "where rowid >= :startRow and rowid < (:startRow + 5)" property int startRow: 0 id: updateQuery times: 10 // execute x times (default is 1) interval: 5000 // delay y milliseconds before each execution (default is 1000) source: _dataDir + "discogs_small.db" queries: [ updateRevision, updateContact, selectRevision ] bindValues: { "startRow" : startRow } onError: console.log("live update error: " + errorType + ", " + errorMessage) onExecuted: { var revision = data[0].revision_id; console.log("live query update was performed: startRow=" + startRow + "; revision=" + revision); listView.dq.emitDataChanged(revision); startRow = (startRow + 100) % 1000; // next time start further down, wrapping at the bottom of the 1024 row table } },
This QueryExec instance is used for the general sql queries that were defined in the global properties, to achieve available functionality of adding a new row of data, or deleting an existing row in combination with selecting the current revision and updating the revision after any change.
// QueryExec instance used for the various SqlDataQuery's QueryExec { id: queryExec source: _dataDir + "discogs_small.db" queries: [ ] onError: console.log("error: " + errorType + ", " + errorMessage) },
These ActionItem are used in order to load the new AsyncDataModel, assign it and it's SqlDataQuery to the ListView for use.
// action item to select the key supported sqlDataQuery ActionItem { title: "sqlDataQuery+key" imageSource: "images/query_with_key.png" onTriggered: { withKeyModel.load() listView.dq = kdq listView.dataModel = withKeyModel } }, // action item to select the revision supported sqlDataQuery ActionItem { //ActionBar.placement: ActionBarPlacement.OnBar title: "sqlDataQuery+rev" imageSource: "images/query_with_revision.png" onTriggered: { withRevisionModel.load() listView.dq = rdq listView.dataModel = withRevisionModel } }, //action item to select the sqlDataQuery supporting both key and revision ActionItem { title: "sqlDataQuery+key+rev" imageSource: "images/both.png" onTriggered: { withKeyAndRevisionModel.load() listView.dq = krdq listView.dataModel = withKeyAndRevisionModel } },
These ActionItem are used to represent the various actions a user can perform on the current data set loaded into the ListView. The live update action allows the execution of the QueryExec instance that performs modifications to the data revision N number of times over an X interval. The delete action launches a toast with an explanatory statement that describes what gesture to use in order to delete a row of data. The Addition actions is self explanatory, the reset action resets the ListView back to the default AsyncDataModel that was used upon application startup.
// action item to activate live updating of revisions ActionItem { ActionBar.placement: ActionBarPlacement.OnBar title: "LiveUpdate" imageSource: "images/ic_edit.png" onTriggered: { if(!liveUpdate) { updateQuery.execute() liveUpdate = true } else { updateQuery.stop() liveUpdate = false } } }, // action item to activate toast describing delete functionality ActionItem { ActionBar.placement: ActionBarPlacement.OnBar title: "delete" imageSource: "images/ic_delete.png" onTriggered: { deleteToast.exec() } }, // action item to reset back to default model ActionItem { ActionBar.placement: ActionBarPlacement.OnBar title: "Reset" imageSource: "images/list_reset.png" onTriggered: { defaultModel.load() listView.dq = defaultDataQuery listView.dataModel = defaultModel } }, // action item to add new row item ActionItem { id: addAction ActionBar.placement: ActionBarPlacement.OnBar title: "Add" imageSource: "images/ic_add.png" // function emits data changed signal with new revision in order // for ListView to refresh so that the addition is visually displayed in the list function onAddExecuted(resultData) { console.log("live record addition ") var revision = resultData[0].revision_id; listView.dq.emitDataChanged(revision); queryExec.executed.disconnect(addAction.onAddExecuted) } onTriggered: { // provide query for inserting new row item queryExec.queries = [ updateRevision, selectRevision, insertQuery] // query bound values containing the new row artist values queryExec.bindValues = { "name" : "Homer" + Math.random().toString(36).substring(2, 8), "realname" : "Baercis", "profile" : "Sr.Attorney at large", "dataquality" : "Correct", "primaryimage" : "blah.jpg"} // connects to the javascript function which emits signal to force ListView to refresh queryExec.executed.connect(addAction.onAddExecuted) queryExec.execute() } }
This is a describtion of the cache window over the data source and how the cache is managed during it's usage with the various SqlDataQuery instances mentioned above.
As you read through the variations of cache behaviour, take note on the amount of items that a query has to retrieve when revisions, item-level keys are used or not. This is where the performance factor comes in when using the optional SqlDataQuery key,revision properties.
Initial cache load and move (no revision or item-level keys)
Cache is being filled from 1st 200 (0-199) items from data source, and creates a threshold line at the 160 mark in cache (20% from edge). When the ListView requests data near the edge of cache, at the threshold mark, the query retrieves next 200 (60-259) ie. "enough data to re-center cache at item 160", the retrieval includes the currently visible items. The cache update to contain items 60-259, and the model reports changes to ListView, which can cause significant disruption in ListView display without using keys or revisions.
Cache move (using revisions)
Cache is filled from 1st 200(0-199) items from data source, ListView requests data near edge of cache(threshold line). The query retrieves next 60 (200-259) ie. "enough data to re-center cache at item 160". As you can tell, the difference between using and not using revisions is significant since only 60 items need to be retrieved versus the previous 200 in absence of revisions. The rest of the cache does not need to be refreshed if no revison change occured.
Cache refresh (using no revisions or keys)
The cache refresh is triggered when source data changes and the dataChanged() signal is emitted. To demonstrate the behavior, lets use the scenario that the cache window is located at 60-259 items, we than add a new block of items(10) to the data source above the cache window (0-60), which pushes the existing items down and overlaps the current cache. This causes the query to retrieve 200(60-259) items in order to refresh entire cache, including currently visible items. Model reports changes to ListView, it's changes in turn could cause significant disruption in ListView display.
Cache refresh (using revisions and keys)
Using the same scenario as above, the query retrieves only the block of new items that were overlapping the cache window. Similar behavior as moving the cache window up by a factor of X additions.
Cache miss (using no revisions or keys)
The ListView requests data not in cache(ie. rapid scrolling). In this instance say our cache window is located at 60-259 items, and a request comes in for data beyond the cache(ie. 260). The query retrieves next 200(160-359) ie. "enough data to re-center cache at item 260"; intermediate queries are discarded if not done and new query request started. Cache update to contain items 160-359 and model reports changes to ListView, which can cause significant disruption in ListView display.
Cache miss (using overall revisions and keys)
The ListView requests data not in cache(ie. rapid scrolling). A request comes in for data beyond the cache(ie. 260), causing the query to retrieve next 100(260-359) ie. "enough data to re-center cache at item 260"; intermediate queries are discarded if not done and new query request started. Cache updated to contain items 160-359 and model reports changes to ListView.