I'm trying to create a database with 3 tables(cars, customers and interventions), then i wanted to create 3 fragments that let you see everything in the database (with a Recyclerview and an item for each tipe of obj)/ populate it (with edit texts).
I've made the database, the classes and the DAO for each class.
My biggest doubt was on the adapters and the fragments
i've had this solution, but it seems extremely complex/convoluted, is there an easier way of doing this?
How i did an adapter:
class CarAdapter(private val cars: MutableList<Car>, private val customers: MutableList<Customer>) : RecyclerView.Adapter<CarAdapter.CarViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CarViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.car_item, parent, false)
return CarViewHolder(view)
}
override fun onBindViewHolder(holder: CarViewHolder, position: Int) {
holder.bind(cars[position], customers)
}
override fun getItemCount(): Int {
return cars.size
}
//method to update the data
fun updateData(cars: List<Car>, customers: List<Customer>) {
this.cars.clear()
this.cars.addAll(cars)
this.customers.clear()
this.customers.addAll(customers)
notifyDataSetChanged()
}
//view holder
class CarViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val carId = itemView.findViewById<TextView>(R.id.tvCarId)
val customerId = itemView.findViewById<TextView>(R.id.tvOwner)
val model = itemView.findViewById<TextView>(R.id.tvModel)
val year = itemView.findViewById<TextView>(R.id.tvYear)
val plate = itemView.findViewById<TextView>(R.id.tvPlate)
//method to bind the data to the view
fun bind(car: Car, customers: List<Customer>) {
carId.text = car.id.toString()
model.text = car.model
year.text = car.year.toString()
plate.text = car.plate
//get the name of the customer
val customer = customers.find { it.id == car.ownerId }
customerId.text = customer?.name
}
}
}
and this is how i did my fragment:
class CarManagerFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
private lateinit var carAdapter: CarAdapter
private lateinit var carDao: CarDao
private lateinit var customerDao: CustomerDao
private lateinit var etModel: EditText
private lateinit var etYear: EditText
private lateinit var etPlate: EditText
private lateinit var etOwner: EditText
private lateinit var btnAddCar: Button
private lateinit var db: Data
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_car_manager, container, false)
//get the dao
db = Data.getDatabase(requireContext())
carDao = db.carDao()
customerDao = db.customerDao()
//get the views
recyclerView = view.findViewById(R.id.rvCars)
carAdapter = CarAdapter(mutableListOf(), mutableListOf())
recyclerView.adapter = carAdapter
etModel = view.findViewById(R.id.etModel)
etYear = view.findViewById(R.id.etYear)
etPlate = view.findViewById(R.id.etPlate)
etOwner = view.findViewById(R.id.etOwner_id)
btnAddCar = view.findViewById(R.id.btnAdd_car)
//set the click listener on the button
btnAddCar.setOnClickListener {
//get the text from the views
val model = etModel.text.toString()
val year = etYear.text.toString()
val plate = etPlate.text.toString()
val owner = etOwner.text.toString().toInt()
//check if the edit texts are empty
if (model.isNotEmpty() && year.isNotEmpty() && plate.isNotEmpty()) {
//create a car object
val car = Car(plate = plate, model = model, year = year, ownerId = owner )
//insert the car in the db
carDao.insert(car)
//update the recycler view
carAdapter.updateData(carDao.getAll(), customerDao.getAll())
//clear the edit texts
etModel.text.clear()
etYear.text.clear()
etPlate.text.clear()
} else {
//show a toast
Toast.makeText(context, "Fill all the fields", Toast.LENGTH_SHORT).show()
}
}
return view
}
override fun onResume() {
super.onResume()
//update the recycler view
carAdapter.updateData(carDao.getAll(), customerDao.getAll())
}
}
its currently giving me this error:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.progettoofficina2, PID: 28043
java.lang.RuntimeException: Unable to resume activity {com.example.progettoofficina2/com.example.progettoofficina2.MainActivity}: java.lang.IllegalStateException: A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:4773)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4806)
at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:54)
at android.app.servertransaction.ActivityTransactionItem.execute(ActivityTransactionItem.java:45)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2307)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7872)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
Caused by: java.lang.IllegalStateException: A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.
at androidx.room.RoomOpenHelper.onUpgrade(RoomOpenHelper.kt:110)
at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.onUpgrade(FrameworkSQLiteOpenHelper.kt:252)
at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:416)
at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:316)
at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableOrReadableDatabase(FrameworkSQLiteOpenHelper.kt:231)
at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.innerGetDatabase(FrameworkSQLiteOpenHelper.kt:189)
at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getSupportDatabase(FrameworkSQLiteOpenHelper.kt:151)
at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.kt:104)
at androidx.room.RoomDatabase.inTransaction(RoomDatabase.kt:629)
at androidx.room.RoomDatabase.assertNotSuspendingTransaction(RoomDatabase.kt:448)
at com.example.progettoofficina2.services.CarDao_Impl.getAll(CarDao_Impl.java:98)
at com.example.progettoofficina2.CarManagerFragment.onResume(CarManagerFragment.kt:84)
at androidx.fragment.app.Fragment.performResume(Fragment.java:3039)
at androidx.fragment.app.FragmentStateManager.resume(FragmentStateManager.java:607)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:306)
at androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:112)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1647)
at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3128)
at androidx.fragment.app.FragmentManager.dispatchResume(FragmentManager.java:3086)
at androidx.fragment.app.FragmentController.dispatchResume(FragmentController.java:273)
at androidx.fragment.app.FragmentActivity.onResumeFragments(FragmentActivity.java:458)
at androidx.fragment.app.FragmentActivity.onPostResume(FragmentActivity.java:447)
at androidx.appcompat.app.AppCompatActivity.onPostResume(AppCompatActivity.java:245)
at android.app.Activity.performResume(Activity.java:8462)
E/AndroidRuntime: at android.app.ActivityThread.performResumeActivity(ActivityThread.java:4763)
I was expecting this to work as intended but it keeps giving me errors on the dao/database even tho they seem alright, i think the issue is somewhere else.
Just in case this is my Room database:
@Database(entities = [Customer::class, Car::class, Intervention::class], version = 2, exportSchema = false)
abstract class Data : RoomDatabase() {
abstract fun customerDao(): CustomerDao
abstract fun carDao(): CarDao
abstract fun interventionDao(): InterventionDao
//get the instance of the database
companion object {
@Volatile
private var INSTANCE: Data? = null
fun getDatabase(context: Context): Data {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
Data::class.java,
"datimacchine"
).build()
INSTANCE = instance
return instance
}
}
}
}
and this is my dao for car (quite similar to the others):
@Dao
interface CarDao {
//get all cars
@Query("SELECT * FROM car")
fun getAll(): List<Car>
//insert a car
@Insert
fun insert(car: Car)
//delete a car
@Delete
fun delete(car: Car)
}
am i complicating my life?
Yes, it does appear that you are.
I believe that the solution is for you to concentrate on smaller parts of the project. Get them working and then proceed to the other parts.
The error that you have shown is because you have increased the version number (as per version = 2
in the Data class) but you haven't provided a migration to alter the schema. In other words, you are telling Room that you have made changes but you haven't told Room how it should apply those changes to the existing database, so it is saying I cannot do what you wish.
This in itself raises some questions:-
@Entity
annotated classes that are listed in the entities
parameter of the @Database
annotated class (or potentially classes, bit not apparently so in your case).So you have 4 permutations:-
Suggestion
I would suggest, at this stage creating all the database code required, obviously Entities, POJO's (see example), Dao's using the main thread (see example/demo below). Once this is all tried and tested (a version number changed suggest this had not been done) then move on.
Example (with a Yes, Yes)
Part one - before the need for change (in this case the introduction of intervention)
Based upon some of your code (not intended to be totally accurate). The following has been tested (at least to some extent):-
Car entity:-
@Entity(
/* Optional but suggested */
/* Adds a Rule (constraint) saying that the ownerId MUST be an existing Customer */
/* if not then a Foreign Key constraint */
/* In database terms this enforces Referential Integrity aka NO ORPHANS */
foreignKeys = [
ForeignKey(
entity = Customer::class, /* The class of the parent i.e. the Customer */
parentColumns = ["id"], /* Customer's id */
childColumns = ["ownerId"], /* Car's reference to Customer id i.e. ownerId */
/* Optional but suggested */
/* Adds automated Referential Integrity management */
onDelete = ForeignKey.CASCADE, /* if Customer is deleted then all the Cars owned by the Customer will be deleted automatically */
onUpdate = ForeignKey.CASCADE /* if (not very likely) the Customer's id is changed then will change the ownerId in the cars owned */
/* by the Customer */
)
]
)
data class Car(
@PrimaryKey
val id: Long?=null,
@ColumnInfo(index = true) /* as ownerid will be used to access a car more efficient to have an index */
val ownerId: Long,
val model: String,
val year: Int,
val plate: String
)
CarDao (perhaps over the top but potentially useful):-
@Dao
interface CarDao{
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(car: Car): Long /* car id (if car id text/string then returns the normally hidden rowid ) */
/* see https://www.sqlite.org/rowidtable.html and/or https://www.sqlite.org/autoinc.html */
/* function to allow Car and it's Customer to be inserted in one function call */
/* Customer if exists according to name and address will be found (EXACT MATCH ONLY) */
/* If not Customer will be inserted and id extracted */
/* Then car will be inserted */
@Transaction
@Query("")
fun insertCarAndCustomer(customer: Customer, customerDao: CustomerDao, car: Car): Long {
val matchingCustomers = customerDao.getCustomersByAddressAndName(customerAddress = customer.address, customerName = customer.name)
var customerId: Long
if (matchingCustomers.isEmpty()) {
customerId = customerDao.insert(customer)
} else {
customerId = matchingCustomers[0].id!! /* should NEVER be null */
}
return insert(Car(id = car.id, ownerId = customerId, model = car.model, plate = car.plate, year = car.year))
}
@Delete
fun delete(car: Car): Int /* number of cars deleted - should be 1 or 0 (if no matching car) */
@Update(onConflict = OnConflictStrategy.IGNORE)
fun update(car: Car): Int /* number of cars updated - should be 1 or 0 if no matching car) */
/* Non convenience @Delete DELETE function (could do similar)*/
@Query("DELETE FROM car WHERE id=:carId OR plate LIKE :wildCharStart||:partOfPlate||:wildCharEnd")
fun delete(carId:Long?,partOfPlate: String="XXXXXXXXXX",wildCharStart: String="",wildCharEnd: String="")
/* defaults set to be least likely to accidentally delete a car */
/* very unlikely to be a plate XXXXXXXXXX (changed to even more unlikely if concerned) */
/* wildChar?? to not be any wildcharacter at all so by default plate must be exact to delete*/
/* if for example delete("a","%","a") then all cars with an a anywhere in the plate would be deleted */
/* use with care */
@Query("SELECT * FROM car")
fun getAllCars(): List<Car>
@Query("SELECT * FROM car WHERE id=:carId")
fun getCarById(carId: Long): Car
@Query("SELECT * FROM car WHERE plate LIKE (:wildCharStart||:partOfPlate||:wildCharEnd)")
fun getCarsWithPlatePart(partOfPlate: String,wildCharStart: String="%",wildCharEnd: String="%"): List<Car>
// and so on as required
}
Customer entity:-
@Entity
data class Customer(
@PrimaryKey
val id: Long?=null,
val name: String,
val address: String
/* and so on */
)
CustomerWithCars POJO used for retrieving a Customer with the Customer's cars (as will be used to demonstrate retrieving ALL data so far):-
data class CustomerWithCars(
@Embedded
val customer: Customer,
@Relation(
entity = Car::class,
parentColumn = "id",
entityColumn = "ownerId"
)
val cars: List<Car>
)
CustomerDao (including getting Customer with Cars, a little awkward due to the split Dao's):-
@Dao
interface CustomerDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(customer: Customer): Long
@Delete
fun delete(car: Car): Int
@Update
fun update(car: Car): Int
@Query("SELECT * FROM customer")
fun getAllCustomers(): List<Customer>
@Query("SELECT count(*) FROM customer")
fun getNumberOfCustomers(): Long
@Query("SELECT * FROM customer WHERE address LIKE :customerAddress AND name LIKE :customerName")
fun getCustomersByAddressAndName(customerAddress: String, customerName: String): List<Customer>
/* Should this be here???? (rhetorical) */
@Transaction
@Query("SELECT * FROM customer")
fun getAllCustomersWithTheirCars(): List<CustomerWithCars>
}
Data - amended to a) run on main thread and b) to check the version in use (to demo method of generating data which could do away with the need to retain the data - as per main activity):-
const val DATABASEVERSION = 1 /* Added to make things a little easier for the demo (can check the intended version in Activity code)*/
@Database(entities = [Car::class,Customer::class], exportSchema = false, version = DATABASEVERSION)
/* NOTE Data is not the best name for the class, is says very little about the class */
/* TheDatabase would be better but still not the best, DB likewise, AppDatabase likewise */
abstract class Data: RoomDatabase() {
abstract fun getCarDao(): CarDao
abstract fun getCustomerDao(): CustomerDao
companion object {
@Volatile
private var INSTANCE: Data? = null
fun getDatabase(context: Context): Data {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
Data::class.java,
"datimacchine"
).allowMainThreadQueries()/* Added for brevity of demo*/.build()
INSTANCE = instance
return instance
}
}
}
}
Activity Code (main activity used for testing):-
class MainActivity : AppCompatActivity() {
lateinit var db: Data
lateinit var carDao: CarDao
lateinit var customerDao: CustomerDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = Data.getDatabase(this)
carDao = db.getCarDao()
customerDao = db.getCustomerDao()
if (DATABASEVERSION == 1 && customerDao.getNumberOfCustomers() < 1) {
loadSomeTestDataOnceOnly()
}
displayData()
}
/* As per the name - load some data (for testing Migration when V2 is run)*/
/* hence check for version and whether or not data exists */
/* version check is probably over the top but perhaps better to be careful */
fun loadSomeTestDataOnceOnly() {
var currentCustomerId = customerDao.insert(Customer(name = "Stirling Moss", address = "10 Brabham Way"))
carDao.insert(Car(ownerId = currentCustomerId, model = "Snipe", year = 1952, plate = "Numer 7"))
carDao.insert(Car(ownerId = currentCustomerId, year = 1960, plate = "Number 7 again?", model = "Berlinetta"))
carDao.insertCarAndCustomer(Customer(name = "Nigel Mansell", address = "The Mansion, Somewhere in England"),customerDao,Car(model = "T570", year = 1977, plate = "33", ownerId = 0))
// and so on
}
fun displayData() {
for(cwc in customerDao.getAllCustomersWithTheirCars()) {
val sb = StringBuilder()
for (c in cwc.cars) {
sb.append("\n\tModel=${c.model} Year=${c.year} Plate=${c.plate} CARID= ${c.id}")
}
Log.d("DBINFO","Customer is ${cwc.customer.name} Address is ${cwc.customer.address} CUSTID is ${cwc.customer.id}${sb}")
}
}
}
Result when run then the output to the log is:-
D/DBINFO: Customer is Stirling Moss Address is 10 Brabham Way CUSTID is 1
Model=Snipe Year=1952 Plate=Numer 7 CARID= 1
Model=Berlinetta Year=1960 Plate=Number 7 again? CARID= 2
D/DBINFO: Customer is Nigel Mansell Address is The Mansion, Somewhere in England CUSTID is 2
Model=T570 Year=1977 Plate=33 CARID= 3
Using App Inspection :-
and :-
Part Two
Due (for the purpose of the Demo) to Interventions being omitted. The Intervention class, the dao need to be added and as data exists a Migration is required to retain the data (for the demo).
So assuming that an Intervention is at the car level and thus an Intervention will be for a Car then.
Intervention class:-
@Entity(
foreignKeys = [
ForeignKey(
entity = Car::class,
parentColumns = ["id"],
childColumns = ["carId"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class Intervention(
@PrimaryKey
val id: Long?=null,
@ColumnInfo(index = true)
val carId: Long,
val description: String
/* and so on */
)
InterventionDao :-
@Dao
interface InterventionDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(intervention: Intervention): Long
@Update(onConflict = OnConflictStrategy.IGNORE)
fun update(intervention: Intervention): Int
@Delete
fun delete(intervention: Intervention): Int
}
CarWithInterventions POJO :-
data class CarWithInterventions(
@Embedded
val car: Car,
@Relation(
entity = Intervention::class,
parentColumn = "id",
entityColumn = "carId"
)
val interventions: List<Intervention>
)
Now as it is likely that retrieval of of Customer along with the Interventions per Car is wanted then CustomerWithCarsWithInterventions (obviously class names could differ to what suits):-
data class CustomerWithCarsWithInterventions(
@Embedded
val customer: Customer,
@Relation(
entity = Car::class,
parentColumn = "id",
entityColumn = "ownerId"
)
val carWithInterventions: List<CarWithInterventions>
)
As this is at the Customer level then the following function added to CustomerDao:-
/* ADDED for V2 */
@Transaction
@Query("SELECT * FROM customer")
fun getAllCustomersWithTheirCarsAndEachCarsInterventions(): List<CustomerWithCarsWithInterventions>
Data class amended as per:-
const val DATABASEVERSION = 1 /* Added to make things a little easier for the demo (can check the intended version in Activity code)*/
@Database(entities = [Car::class,Customer::class,/*ADDED for V2>>>>>*/ Intervention::class], exportSchema = false, version = DATABASEVERSION)
and :-
abstract fun getInterventionDao(): InterventionDao /*<<<<< ADDED for V2 */
Ooops 001
Compile and run and :-
Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number. ....
Increase the Version number to 2:-
const val DATABASEVERSION = 2
Compile and run and :-
A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations ....
So roughly the error in the question has been reproduced at this stage.
Part 3 _moving forward:-
Room needs to be told what to do. The change (as all has compiled OK so far (P.S. no warning in the build log)) is that (in this case) the new intervention table needs to be added to the database.
How? (rhetorical), Well you could try working out the table should be and then how to create it with a Migration and so on. The first part is not that easy (actually it is, as will be shown) due to Room being very picky about the columns indexes etc should be (the all too often encountered `Room .... expected .... found .... error).
No need to go there because Room actually tells you what it expects. When you compile the project Room (the annotation processing) generates Java that includes a class the same name as the @Database
annotated class but suffixed with _Impl. In that class is a method (function) named CreateAllTables. e.g. (via Android View in Android Studio):-
As can be seen in the Migration we need to do 2 things:-
The Migration
A Migration is required and Room needs to be told about it.
First an empty Migration for the version being changed from 1 to 2:-
val migrationFromV1ToV2Config = object: Migration(1,2){
override fun migrate(database: SupportSQLiteDatabase) {
TODO("Not yet implemented")
}
}
Add the two executions using the SupportSQLiteDatabase
(database
) passed to the Migration:-
val migrationFromV1ToV2Config = object: Migration(1,2){
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `Intervention` (" +
"`id` INTEGER, `carId` INTEGER NOT NULL, `description` TEXT NOT NULL, " +
"PRIMARY KEY(`id`), " +
"FOREIGN KEY(`carId`) REFERENCES `Car`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
database.execSQL("CREATE INDEX IF NOT EXISTS `index_Intervention_carId` ON `Intervention` (`carId`)")
}
}
Tell Room about the migration via the databaseBuilder :-
.... ).allowMainThreadQueries()/* Added for brevity of demo*/.addMigrations(
migrationFromV1ToV2Config) /*<<<<< ADDED for V2*/ .build()
Now assuming the SQL was inadvertently altered (table named accidentally changed to NOTIntervention
) then Oooops:-
android.database.sqlite.SQLiteException: no such table: main.Intervention (code 1 SQLITE_ERROR): , while compiling: CREATE INDEX IF NOT EXISTS `index_Intervention_carId` ON `Intervention` (`carId`) ....
Now assuming corrected but another inadvertent but more subtle error (NOT NULL added to the id column) then Oooops:-
java.lang.IllegalStateException: Migration didn't properly handle: .... Expected:
TableInfo{name='Intervention', columns={description=Column{name='descript .... Found:
TableInfo{name='Intervention', columns={id=Column{name='id', type='INTEGER', affinity='3'
Correct the SQL to be EXACTLY as per the SQL in the generated java and it works. App Inspection shows:-
You would (if following the suggested methodology) then extract the data using the getAllCustomersWithTheirCarsAndEachCarsInterventions
.
Then you could start working on the UI view models etc knowing that the database and dao's work as intended.