We're sorry but this page doesn't work properly without JavaScript enabled. Please enable it to continue.
Feedback

Data Persistence in Android

00:00

Formal Metadata

Title
Data Persistence in Android
Title of Series
Number of Parts
52
Author
License
CC Attribution 3.0 Unported:
You are free to use, adapt and copy, distribute and transmit the work or content in adapted or unchanged form for any legal purpose as long as the work is attributed to the author in the manner specified by the author or licensor.
Identifiers
Publisher
Release Date
Language

Content Metadata

Subject Area
Genre
Human migrationBoilerplate (text)CodeThread (computing)Operations researchDatabaseThread (computing)Mobile appOperator (mathematics)DatabaseMultiplication signCursor (computers)Android (robot)Latent heatHuman migrationString (computer science)Software developerComputer virusInterior (topology)Software testingBoilerplate (text)JSONUML
MereologyConnectivity (graph theory)Library (computing)Query languageComputer architectureHuman migrationLatent heatSet (mathematics)Focus (optics)BitoutputComputer animation
DatabaseComponent-based software engineeringString (computer science)Connectivity (graph theory)Level (video gaming)Table (information)Social classInstance (computer science)Row (database)Key (cryptography)DatabaseCodeComputer animation
String (computer science)Query languageKey (cryptography)Table (information)Database1 (number)Social classConstraint (mathematics)Default (computer science)BitMathematicsCartesian coordinate systemSubject indexingVariable (mathematics)Level (video gaming)Insertion lossHuman migrationException handlingQuery languageInformationSystem administratorJava appletJSON
Query languageString (computer science)Software testingInterface (computing)Object (grammar)Table (information)DatabaseSubsetEndliche ModelltheorieSocial classSet (mathematics)ExistenceData modelQuery languageMultiplication signElectronic mailing listMoving average
Revision controlAbstractionContext awarenessJava appletDatabaseRevision controlPositional notationInstance (computer science)Query languageMultiplication signTable (information)Insertion lossSocial classObject (grammar)Open set
Data modelQuery languageState of matterDatabase transactionOperator (mathematics)Thread (computing)Instance (computer science)DatabaseEndliche ModelltheorieQuery languageImplementationElectronic mailing listInterface (computing)Social classException handlingJSON
Query languageInterface (computing)Interior (topology)Thread (computing)HTTP cookieBitQuery languageLevel (video gaming)ResultantError messageComputer architectureSingle-precision floating-point formatElectronic mailing listObject (grammar)MereologyState observerThread (computing)Element (mathematics)CASE <Informatik>InformationMultiplication signLatent heatException handlingTable (information)DatabaseConnectivity (graph theory)Java appletComputer animation
Software testingDatabaseBuildingJava appletRule of inferenceRepository (publishing)CodeSocial classSampling (statistics)Software testingRule of inferenceQuicksortParameter (computer programming)Connectivity (graph theory)Computer architectureAndroid (robot)Thread (computing)DatabaseInstance (computer science)In-Memory-DatenbankUnit testingTask (computing)Constructor (object-oriented programming)Repository (publishing)Process (computing)Survival analysisMultiplication signUtility softwareLevel (video gaming)Execution unitComputer animationJSON
Mountain passRule of inferenceINTEGRALSoftware testingImplementationBitHuman migrationRule of inferenceConnectivity (graph theory)Computer architectureTask (computing)Social class
Human migrationRevision controlUsabilityAbstractionDatabaseJava appletBuildingCartesian coordinate systemHuman migrationOpen setRevision controlDatabaseTable (information)Hash functionMathematicsLevel (video gaming)BitObject (grammar)Electronic mailing listMultiplicationCASE <Informatik>Computer animation
Human migrationHash functionRevision controlJava appletHash functionDatabaseRevision controlException handlingHuman migrationMoment (mathematics)Cartesian coordinate systemMultiplication signState of matterMathematicsCASE <Informatik>NumberTable (information)Query languageDefault (computer science)Software testingIdentity managementNeuroinformatik
Human migrationSoftware testingCanonical ensembleJava appletDatabaseBuildingHuman migrationMereologySoftware testingDatabaseHash functionAndroid (robot)Revision controlInformationTable (information)Level (video gaming)INTEGRALIdentity managementValidity (statistics)Expected valueSocial classUnit testingQuery languageCASE <Informatik>Computer animation
CodeSample (statistics)Google TalkCubeStatement (computer science)Revision controlHuman migrationIntegrated development environmentView (database)Data storage deviceComputer architectureCASE <Informatik>DatabaseFlagNumberSocial classLatent heatRight angleProduct (business)Validity (statistics)QuicksortMobile appCartesian coordinate systemMultiplication signPoint (geometry)ImplementationSign (mathematics)Mechanism designDifferent (Kate Ryan album)Table (information)System callEvent horizonBoilerplate (text)Bus (computing)Java appletGoogolBuildingConnectivity (graph theory)Presentation of a groupBeta functionParameter (computer programming)CodeSpacetimeQuery languageSoftware testingThread (computing)Phase transitionSampling (statistics)Operator (mathematics)BitPlanningInverter (logic gate)Software bug2 (number)MathematicsElectronic program guideSource codeDesign by contractTunisVirtualizationSemiconductor memorySet (mathematics)Computer virusSinc functionMultilaterationJSONXMLUML
Transcript: English(auto-generated)
Hello. Thank you for the introduction. I want to start with a confession. For a long time in my Android developer career, working with databases just felt like there was so much boilerplate that I had to write. All of those cursor get int of that specific column,
cursor get string, and so on. Implementing migrations was kind of difficult. It always made me feel like whenever I'm changing something in my database, I'm defusing a bomb because you never know what happens when the app actually
gets in the hands of the user. And that was also because it is possible to test SQLite database on Android, but it's not the easiest thing in the world. And then I actually did this. I actually did database operations on main thread. I do hope this stays between us and you're not going to tell anyone that I did this, please.
So because of all of these problems, Room appeared. Room is one of the libraries part of the architecture component set of libraries. And what I want to tell you today is a little about the basics, just to make sure that we all know what's
up with all of these components. But I want to focus on two things in specific. I want to focus on the queries and on migrations. I want to tell you how they work under the hood and why you should care about how they work under the hood. And also, I'll also mention all of the stuff that we've been doing with Room since they were initially announced at Google IO.
So let's start with the components. Let's say that we have a dessert class. And this dessert class has an API level and a name. We know which kind of dessert we're talking about. And what I want to have is a table in my database that's called desserts that also
has a column called API level and a column called name. And of course, that API level should be the primary key of that table. But actually, what I want is that every row of that table is an instance of that dessert class. To do this in Room, we just use the fEntity annotation.
So more precisely, we annotate that Pogo class with fEntity. We can mention the table name. And then for every column, we can say whether it is a primary key. And we can use the fColumnInfo annotation to tell which one is the name of the column.
If you don't put any name in the column info, so like I've done here with the name of the dessert, then Room by default will just use that variable name to set that as the column name. As a pro tip, I'm saying always use the column info, because if you're like me and do
any typos in the variable names, you can easily change that without actually changing the database schema. So Room will see which kind of classes are annotated with entity, which ones you declare as tables. And we'll do all the heavy lifting for you. Room will be the one that creates a table for you,
will be the one that sets the primary key. If you have any constraints like indexes, Room will take care of those for you. But there's one more thing to notice here. If you're using Kotlin, you know that this means that this is a non-null variable, because it doesn't have that question mark. Well, this means that if in my table
I'm trying to insert a dessert that has a null name, what will happen is get the SQLite constraint exception, because Room checks whether you have an add non-null or nullable or non-nullable column, and then sets those constraints on the columns.
So if you have a nullable, so you have that question mark in Kotlin, then if you are trying to insert a dessert that's null, everything's OK. If you're using Java, Room will just take care of the same things using the add non-null or nullable annotations.
So this means that if you're developing your application, and then all of a sudden you're saying, OK, this variable, this name thing, I know for sure it's not null, and then you're adding this constraint, this will change the schema of your table.
So this means that you'll have to implement the migration for this. So this small change that can seem quite innocent can trigger some more work than you want. I'll tell you more about migrations in a bit. So what can you do with Room? We have our table. We want to access it. So we want to provide that query, insert, update, delete
methods. We do this with the atDao annotation. Dao stands for data access object. So more precisely, we would create an interface or an abstract class that's annotated with Dao. And here we declare the methods that query our database.
So we don't really need to return just the entities like dessert, but we also can return a smaller set of columns. So for example, we can just get the dessert names and return a list of strings. Or if our dessert is more complicated
and we want to display it on the UI, we can just return a smaller subset and just get, let's say, a dessert UI model. So the object that can be returned from the query can be an entity, but also a data model that you define. But one thing I do a lot is typos and bad copy pastes.
So I end up with maybe column names that don't really exist or table names that don't exist. But luckily with Room, these are already detected at compile time. So this means that I don't need to actually run my app or run the tests to see that I did some wrong typo there.
OK, so we defined our entities, but then how do we actually construct that table? Well, we do this with the at database annotation. More precisely, we construct an abstract class that extends from the Room database. And there in the at database annotation,
we define which are the entities, so which are the tables of our database. So this means that if you're creating more entities than you're declaring there, Room will just don't use them and will not create tables for those tables if you don't define them.
And also there, we define the database version. Also in our Room database, we define the data access objects. So which are the DAOs that work with that database? One thing to note is that you should work with a singleton, with a single instance
of the database. So chances are if you're using Dagger, you will probably create a singleton in Dagger. If not, just make sure you're working with a singleton because, well, let's say SQLite databases don't work so nicely if you have multiple connections open in the same time and you want to avoid this. So that's why you should have one database instance at one
time. OK, let's see what's up with these queries. So let's start with insert, update, and delete because they work quite similar. So in our interface, we define how to insert a dessert, how to update a dessert, and how to delete it.
So what's going on under the hood? So what Room does is provide the implementation for this class. It will generate it for you. And it will work with a Room database, which is actually an instance of the database that you defined before. And it will implement that method for you. So for example, when you want to insert a dessert,
it will do begin transaction. It will insert that dessert in the database. It will set transaction successful. And then it will end it. What I really want you to notice here is that Room is the one that handles the transaction for you. So this means that if you want to insert a list of desserts,
automatically, Room will make sure that this is all happening in one transaction. But what if you want to handle that by yourself? So let's say that you want a dessert to be added and a dessert to be removed. And you want all of this to happen atomically
in one transaction. Well, Room gives you the flexibility to do this by calling run in transaction on a database instance. And that's all. It will make sure that everything happens as you want. Let's talk about the threading model. Well, insert, update, and delete are synchronous.
And, well, spoiler query can also be synchronous. So what does this mean? It means that these queries will be run from the same thread you are calling them from. So if you're doing this from the main thread, you'll get an illegal state exception, because Room is really making sure
that you're not doing any long running operations on the main thread. OK, I already mentioned a little bit about query. Let's see more about it. So let's go with a more complicated query. Let's say that I want to get a dessert for a specific API level.
So I can do this in several ways. I can do this synchronously by just calling getDessertByAPILevel that returns me the dessert. But I can also do this asynchronously. So I can do this by returning a maybe or a single, but also asynchronously via observable queries.
So these observable queries return live data, which is a data holder object, also part of the architecture components, or a flowable from RxJava. OK, let's dive a bit more into the maybe and the single and see what's going on here. So let's say that our table looks like this.
So I have two desserts in the database, one for API level 3 and one for API level 4. And what I want to do is get the dessert for API level 3. Our maybe and our singles will just return on success that object, and then they will complete. Things are, of course, a bit different when I want to get an object that's not there.
So let's say that I want to get the dessert for API level 26. With the single, we will get an exception. We will get an on error with empty results set exception. And the maybe will just complete. So make sure that you use the one that you really need for the case that you want to handle.
Let's look at live data and flowable. So we're going to get the same case of getting the dessert for API level 3. For this, they both behave the same. They just return you that object for API level 3. But then whenever you're modifying the dessert,
so for example, instead of cupcake, we change it to cookie. When you're using observable queries, you will automatically get another object, another notification with that new object. So every time you're modifying the database,
this will automatically be reflected in the observers. Everyone will get notified of this. But if we're getting an object that doesn't exist in a database, so let's say API level 26 with a live data, what happens here is we just get null because we don't have that object.
But if you're using a flowable, nothing happens because we all know flowables can't support null. So this means that, okay, if we're changing the data and then we're actually inserting a valid element
in the database, afterwards we will get that information. So then what do you do? How do you handle this? One solution, if you actually want to get notified when the data is missing, is using a list. So in this case, for getting the deserve by API level 26,
you will get an empty list, and then whenever you actually have items, you will get a list with those items. Okay, let's talk about the threading part. So where things happen. One thing that Room guarantees for you is that both if you're using a flowable or a live data,
the actual query itself is happening on the background thread. But then if you're using a live data, the result will be given on the main thread, so you'll be notified on this on the main thread. With RxJava, you're just gonna get the result on whatever you set on observeOn.
Okay, we've defined our DAOs, we've defined our entities, we need to test them. Let's see how we do this. So we need to implement an Android JUnit test for this, and here we would have a reference to the database and to the DAO. One thing that Room is really leveraging is using the in-memory database.
So more precisely than the before, we would create an instance of the Room database, and then after we would just close it. So what does this in-memory database do? What it does is actually make sure that the data that you're putting in the database will not survive process death. So this makes it perfect for tests,
because it means that even if you're inserting data in a test, you will need to clean it up afterwards. Room will just do this for you. How can we implement a test? Let's say that we want to check whether indeed a dessert can be retrieved after it has been inserted. So we will insert a dessert,
and then we will check that indeed when we retrieve the dessert based on the API level, this dessert can be correctly retrieved. So the dessert is the expected one. Okay, what happens when you use LiveData? Well, we're inserting the dessert, of course,
and then we're calling dow.getDessert by API level. But here, this is an important rule that you could use, the test rule, the InstantTaskExecutor test rule. So this is a class that's provided by the architecture components that replaces the background executor with an instant one. So it makes sure that you don't need to wait
for this to happen somewhere on a background thread. And then, getting back to our test, you can easily get the actual dessert and then check that's a valid one. So that's LiveDataTestUtil. It's a method that we've, sorry, it's a class that we've implemented in one of our sample codes.
And just for the sake of simplicity, I didn't put it here, but check it out. It's quite simple. Okay, what happens when we actually use RxJava? So we have the same test, getting the dessert after it was inserted. So we're inserting it, and we're trying to get it. Also here, we would use the same InstantTaskExecutor rule
that makes sure that the task is being retrieved instantly. And then, we're just checking that the data that we inserted is the one that we retrieved. A lot of times, we end up using our DAOs in other classes, like maybe in a sort of a repository class.
One of the benefits of DAOs is that they can easily be mocked, so you can actually unit test that class that uses the DAO. So for example, if you were to implement the test for the dessert repository, all you can do is just use Mockito to mock the DAO, and then use that instance as a parameter
in the constructor of the repository. And then from there on, it's just a simple unit test. What happens when you actually use Expresso and you want to implement integration tests? We have another handy test rule, which is called CountingTaskExecutor rule.
And this test rule actually swaps the background executor used by architecture components with another one that counts the tasks that are started and finished. Here, you'll have to do a little bit more work, because you'll have to actually extend this class. And then whenever the test is starting,
then you'll have to register your idling resources. But there isn't that much work to do on this. Let's see what's going on with migrations. Let's first understand exactly what migrations are and when do you need to implement what.
So the thing is that our applications are never static. They always evolve. They always change. And also, the way we're storing the data tends to change. So let's say initially we had this API level and the name. Let's say that we also need to add another column, which is the release date. So this means that the schema changes.
And if the schema changes, the version of the database needs to change. And if the version of the database needs to change, you should provide a migration. So when you are using the SQLite API, you would have to do something like this. You would extend the SQLite, open helper. And then in onUpgrade, you would actually
tell SQLite what should happen when it goes from version one to version two. And things were quite easy when you only had two versions or three versions. But when you actually started having multiple versions, things became quite complicated. So that's why in Room, we added a migration object.
So a migration object handles the migration from version x to version y, so only one step, let's say. You would implement that object with, let's say, going from version one to version two. And then you would define what should happen in that migration.
So for example, with our dessert case, I'm just adding a new column, the release date. But then you would also add that migration to the Room database, because Room needs to know that it should use that migration. Let's go a bit in the internals of the database
of the migrations. So what actually Room does for you is create a version hash corresponding to a specific table. So this means that that version hash will be held inside the Room master table. So don't be surprised if you're getting the database from your application, and then you see some other table here, like the Room master table,
that you don't know of. And then when you're adding migrations, so you can add there also a list of migrations. What Room will do for you is when the application started,
that's not actually the moment when the migrations are triggered. The migrations are triggered when the database is built. And the database is built at the first time when you're querying the database. So even if you're declaring that builder somewhere, I don't know, maybe in the application start, nothing will happen until you actually
get to query the database. And Room provides a nifty thing. So if you have multiple migrations, so sorry, let's say you have from 1 to 2, 2 to 3, 3 to 4, but also 2 to 4, what Room will do is make sure it takes the shortest path. So it would only use the migration from 1 to 2 and 2
to 4 to get from version 1 to version 4. So what does this mean? It means that when you're doing that first query, it doesn't need to go through all of these steps of migrations of maybe copying data from one table to another or whatever you did through all of these migrations. But rather, it can take the shortest path.
And when it does this, it will change the schema of your database. But it will also update that version hash in the Room master table. Let's take a scenario where you're changing the schema. So for example, you're changing your entities,
but you forget to update the version number of your database. So in this case, what Room will do is check, hey, what's the version hash of this database? And it will see that it's version 1, but that's not corresponding to how the table looks.
It's not the same one as the identity hash that Room computes for those entities. And you will get an exception. More precisely, you'll get an illegal state exception because you didn't change the version number. Let's say that you're going from version 1 to version 4,
but you're not providing any migration. So in this case, Room will throw an illegal state exception because Room doesn't know how to handle the entities in that case. It doesn't know how to generate the migration. So it doesn't know, for example, what are the default values that it should put for those columns that you set there.
But what you can do, if you're OK with losing users' data, I hope you're not OK with losing users' data. But if you are, you can use the fallback to destructive migration. So this means that if you're not providing a migration, the database will just be dropped and recreated.
I think one case where this is useful is also for testing. So if you quickly want to check out how it works, how the database looks, it can be useful. But I would strongly advise on keeping the users' data. OK, how do we actually test the migrations?
For me, actually, this was the most painful thing and the part that I always feared when I was using the SQLite API. Well, what Room does for you is actually generate a JSON that contains the information about the tables. And more precisely, it also has that identity hash
that I've been mentioning before. And to actually test the migration, you implement an Android JUnit test. And then inside, you will use another test rule, more precisely, the migration test helper. So this class will help you with a lot of things.
It will help you create a database in a specific level. It will help you run the migrations and validate them. So for example, a test would look something like this. So let's say that we want to check if the migration from version 1 to version 4 really contains the correct data.
So in this case, we would create our database in version 1. We would insert the user. Sorry, we would insert the dessert. And then we would actually run those migrations and validate them. So actually, what Room is doing here is just checking the database schema integrity.
So it just checks that indeed things look as expected. But Room doesn't check the validity of the data in the table. So this is something that you will need to do. So you will actually need to get the reference to the database and then actually query the database and check that the data you inserted initially is there.
And maybe some other columns that you added are there correctly. So that's pretty much how Room works. It helps you with a lot of things. It helps you write way less boilerplate code. It gives you compile time checked queries.
It's way easier to implement migrations. And it's, in general, way easier to test the DAOs and to test the migrations. And also, it makes sure that you're not doing mistakes that I used to do, like do database operations on main threads. We've been writing about this a bit on Medium.
We have our guides to app architecture. And we've been writing a few samples and some code labs. So check them out. Start using Room, either with Java or with Kotlin, and let me know how it went. Thank you.
Yeah. So we still have time for questions. If someone of you wants to make one question, there are two microphones, one here and one there. If you want to, there are some. You can.
OK. Hi. That was a great talk. And this is kind of promising, because I am just looking for a new approach for the handling architecture storages and stuff.
My question is, because you have used Kotlin for your examples, right? My first question is, are there any implication for using Java or Kotlin, or this is basically interchangeable? Sorry, are you getting what? Is this any recommendation for use Java or Kotlin here,
or it doesn't really matter? No, you can use whatever you want. If you're using Java in your application, start using Room with Java. If you're using Kotlin, you can use Room with Kotlin. There is no difference in the features that they support. So it's the same. OK.
So the second question would be, because I have just quickly looked around this Google architecture approach, it seems that it's still in beta phase, right? And yes, exactly. OK, so this is kind of argument against using it in the production environment when something goes wrong. The customer comes and see why you're actually using the beta
technology anyway. So do you have any roadmap for having it final? Yeah, we're planning it on releasing the stable version very soon. I can't actually give you a date. But I can tell you that we're not planning any API changes. Until then, we will have probably some minor bug fixes,
but it's soon. Because it looks very promising to me, but there are really often statements in the contract that basically, if you are not using any stable environment, this is basically your fault when something goes wrong. So this looks very promising, and hopefully we're
going to find the final version soon. Thanks. Hello. I definitely agree on that, how promising it is. My question is that the migration fallback. It is safe to set, and it will
be called unless we don't provide a specific migration. I'm not assuming wrong in this case. So if we have a migration from one version to another specifically, it will call that. Otherwise, it will call the fallback. So let's say that you're going from version 1 to version 4. You have migrations from version 1 to version 2
implemented, but not for the others. If you're sending that flag with fallback to destructive migration, your database will be wiped, independent on whether you implemented other migrations before or not. OK, so if I have a migration from 1 to 2, or 1 to 3, and I set the fallback.
If you set the flag, the fallback, it will anyway be clear, independent on whether you changed something there or not. Would be nice if we could prioritize that. In the same time, the idea with the fallback to destructive migration is that it cleans your database. And then it's recreated with the latest version, so with, let's say, version 4.
So even if you're providing a migration from version 1 to version 2, if you're still saving data there, it will still going to be wiped later on. So it doesn't really make sense of having that. Yes, sure. But my point is, for example, if I normally don't care, but from really specific from one version to another,
like from 2 to 3, I care to migrate something. But the rest, from 3 to 5 or whatever, I just drop. I think I know what you mean now. I saw a feature request on allowing destructive migrations only for a specific version. I saw that there is, so for example, let's say if your app is going from version 3 to version 4,
you don't care about the data. But you do care about the data if it's from version 1 to version 3. So let's say, destroy the table only for a specific version. So right now, we don't have that implemented. There's a feature request for this. I don't know when it's going to be done. OK, cool. I hope it will be done. Thank you.
Do we still have time? We still have time for one question. OK, sorry. You're the lucky last question. I have two small questions. One is, since it's based on annotations processing, I'm wondering on how does the build time impact look like? So have you ever measured it? I don't have any numbers to give you, sorry.
All right. OK, then second question. I noticed that you used live data from the components, architecture components, and the race Java example. So my question is, is it possible to get the data asynchronously, but not depend on those two?
Yes. Like some sort of callback, like old style callback? Room case, or it's using actually under the hood a class that's called the invalidation tracker. So the short version implement in your Dao something like flowable or live data. And then if you check the implementation,
so your Dao underscore in pollute, you will see there that there's a class called invalidation tracker that's being used for this. And if I'm not mistaken, this class is public, so you should be able to implement your own mechanism that actually relies on this invalidation tracker. So you can do, I don't know, event bus, space, or something else.
OK, thanks. Sure. Hello, thank you for the great presentation. So I'm eager to test Room in my apps, but first I want to ask, I'm using virtual tables in my applications, actually in all of them. So will you support that before the final release?
So virtual tables will not be supported for the 1.0 release. OK, and I have a small second question. So for using Room with recycled views, what do you recommend? Because I'm currently using cure source. Will you provide some kind of class to avoid loading the whole data set in memory for these views,
or recycled views? Yes. Stay tuned. OK, thank you. I think that if it's really, really, really fast, a quick question, maybe we still have time to do that. No worries, I can just talk to Florina later.
Then thank you again to Florina and all of the remaining member of the audience. And I hope you will have a nice convention. So an applause again, and thank you.