Getting Random android.database.sqlite.SQLiteBlobTooBigException - android

I am using Room in my app as a Single Source of Truth, so everything that comes from the backend I save in my room database, which then returns a Flowable that fires an event every time the data changes. This is my PlacesDAO:
#Dao
abstract class PlacesDao {
#Query("select * from places where placeId = :id")
abstract fun getPlace(id: String): Flowable<PlaceVO>
#Query("select * from places where placeId in (:placesIds) order by distance, placeName ASC")
abstract fun getPlaces(placesIds: List<String>): Flowable<List<PlaceVO>>
#Query("select * from places join list_results where Places.placeId = list_results.id order by distance, placeName ASC")
abstract fun getFilteredPlaces(): Flowable<List<PlaceVO>>
#Query("select * from places join user_places where Places.placeId = user_places.placeId AND user_places.userId = :userId order by distance, placeName ASC ")
abstract fun getAllByUser(userId: String) : Flowable<List<PlaceVO>>
#Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun realSavePlaces(places:List<PlaceVO>)
fun savePlaces(places: List<PlaceVO>){
Timber.w("PAGELIST - Saving places again!!")
realSavePlaces(places)
}
#Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun savePlace(place: PlaceVO)
#Query("DELETE from places")
abstract fun deleteAll()
#Query("select * from places")
abstract fun getAll(): Single<List<PlaceVO>>
#Query("select * from places where (experienceId IS NOT NULL) AND (experienceId != '') order by placeName")
abstract fun getMyPlaces(): Flowable<List<PlaceVO>>
#Query("update places set distance = :distance and distanceSet = 1 where placeId = :id")
abstract fun updateDistance(id: String, distance: Float)
}
Now in my app theres a few actions that would trigger changing the data in this table, which then causes my UI to receive all items contained in the table (around 3000-5000).
It does niot always happen (which makes it hard to reproduce) but every now and then I will get the following crash:
Caused by android.database.sqlite.SQLiteBlobTooBigException: Row too big to fit into CursorWindow requiredPos=1223, totalRows=114
at android.database.sqlite.SQLiteConnection.nativeExecuteForCursorWindow(SQLiteConnection.java)
at android.database.sqlite.SQLiteConnection.executeForCursorWindow + 895(SQLiteConnection.java:895)
at android.database.sqlite.SQLiteSession.executeForCursorWindow + 836(SQLiteSession.java:836)
at android.database.sqlite.SQLiteQuery.fillWindow + 62(SQLiteQuery.java:62)
at android.database.sqlite.SQLiteCursor.fillWindow + 157(SQLiteCursor.java:157)
at android.database.sqlite.SQLiteCursor.onMove + 128(SQLiteCursor.java:128)
at android.database.AbstractCursor.moveToPosition + 237(AbstractCursor.java:237)
at android.database.AbstractCursor.moveToNext + 269(AbstractCursor.java:269)
at com.myapp.android.model.db.dao.PlacesDao_Impl$6.call + 814(PlacesDao_Impl.java:814)
at com.myapp.android.model.db.dao.PlacesDao_Impl$6.call + 771(PlacesDao_Impl.java:771)
at io.reactivex.internal.operators.maybe.MaybeFromCallable.subscribeActual + 46(MaybeFromCallable.java:46)
at io.reactivex.Maybe.subscribe + 4262(Maybe.java:4262)
at io.reactivex.internal.operators.flowable.FlowableFlatMapMaybe$FlatMapMaybeSubscriber.onNext + 132(FlowableFlatMapMaybe.java:132)
at io.reactivex.internal.operators.flowable.FlowableObserveOn$ObserveOnSubscriber.runAsync + 407(FlowableObserveOn.java:407)
at io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber.run + 176(FlowableObserveOn.java:176)
at io.reactivex.internal.schedulers.ExecutorScheduler$ExecutorWorker$BooleanRunnable.run + 260(ExecutorScheduler.java:260)
at io.reactivex.internal.schedulers.ExecutorScheduler$ExecutorWorker.run + 225(ExecutorScheduler.java:225)
at java.util.concurrent.ThreadPoolExecutor.runWorker + 1167(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run + 641(ThreadPoolExecutor.java:641)
at java.lang.Thread.run + 764(Thread.java:764)
I am only storing text info, as proved by this class:
#Entity(tableName = "places")
data class PlaceVO(
#PrimaryKey
var placeId: String,
var googleId: String,
var placeName: String,
var phoneNumber: String,
#Embedded
var primaryCategory: Category?,
var primaryCategoryTags: List<CategoryTag> = emptyList(),
var secondaryCategories: List<Category>? = emptyList(),
var images: List<Image>,
var website: String,
var formattedAddress: String? = "",
var vicinity: String = "",
var vicinityShort: String = "",
var city: String? = "",
var neighbourhood: String?,
var longitude: Double,
var latitude: Double,
var openingHours: List<String>,
var combinedHighlights: List<HighlightCountWrapper>,
#Embedded
var ownExperience: OwnExperience?,
var otherExperiences: List<Experience>,
var distance: Float?,
var distanceSet: Boolean = false,
var comment: String
) : MarkerPlace {
}
Experience class:
#Entity
data class Experience(
#Json(name="id")
val experienceId: String,
#Embedded
val owner: User,
val description: String?,
val highlights: List<Highlight>?,
val images: List<Image> = emptyList(),
val createdDate: Date,
val updatedDate: Date,
var privacyLevel: AddExperience.Privacy? = null)
Some TypeConverters:
#TypeConverter
fun toHighlightWrapperList(value: String): List<HighlightCountWrapper> {
val type = Types.newParameterizedType(List::class.java, HighlightCountWrapper::class.java)
return moshi.adapter<List<HighlightCountWrapper>>(type).fromJson(value) ?: emptyList()
}
#TypeConverter
fun fromHighlightWrapperList(list: List<HighlightCountWrapper>): String {
val type = Types.newParameterizedType(List::class.java, HighlightCountWrapper::class.java)
var adapter: JsonAdapter<List<HighlightCountWrapper>> = moshi.adapter<List<HighlightCountWrapper>>(type)
return adapter.toJson(list)
}
#TypeConverter
fun toExperienceList(value: String): List<Experience> {
val type = Types.newParameterizedType(List::class.java, Experience::class.java)
return moshi.adapter<List<Experience>>(type).fromJson(value) ?: emptyList()
}
#TypeConverter
fun fromExperienceList(list: List<Experience>): String {
val type = Types.newParameterizedType(List::class.java, Experience::class.java)
var adapter: JsonAdapter<List<Experience>> = moshi.adapter<List<Experience>>(type)
return adapter.toJson(list)
}
#TypeConverter
fun toImageList(value: String): List<Image> {
val type = Types.newParameterizedType(List::class.java, Image::class.java)
return moshi.adapter<List<Image>>(type).fromJson(value) ?: emptyList()
}
#TypeConverter
fun fromImageList(list: List<Image>): String {
val type = Types.newParameterizedType(List::class.java, Image::class.java)
var adapter: JsonAdapter<List<Image>> = moshi.adapter<List<Image>>(type)
return adapter.toJson(list)
}
so how can it be that my rows are too big for SQLite? Especially when sometimes exactly the same data will be returned without a problem?

I found how to use length() and substr() to request only 1MB (the max for CursorWindow is 2MB), maybe it will help. (in your case you could simply divide the requests into chunks of 100 rows and then close the cursor, then repeat)
It seems that you are storing images, in which case it would be better to store them in internal storage, and only store the file path in the database.
Especially when sometimes exactly the same data will be returned without a problem?
If you mean exactly the same rows (like the rows from 1000 to 2000), but from different devices, it might be the case that they have different "max-sizes" for the CursorWindow, in my case it seems to be 2MB.

Related

How can I add data into Room database in a manner where it is appended to the top row

Image for reference
I am facing a situation whenever I add a new article into the database it will add the new article to the bottom of the RecyclerView, how can I implement it in such a way that it updates the top row instead?
Is it something got to do with the Adapter or do I have to tweak the Doa?
Room Database Implementation
**//ViewModel**
fun saveArticle(article: Article) = viewModelScope.launch {
newsRepository.upsert(article)
}
**
//NewsRepository**
suspend fun upsert(article: Article) = db.getArticleDao().upsert(article)
**//Implementation of the Database**
#Database(
entities = [Article::class],
version = 1
)
#TypeConverters(Converters::class)
abstract class ArticleDatabase : RoomDatabase() {
abstract fun getArticleDao(): ArticleDao
companion object {
#Volatile
private var instance: ArticleDatabase? = null
private val LOCK = Any()
operator fun invoke(context: Context) = instance ?: synchronized(LOCK) {
instance ?: createDatabase(context).also { instance = it }
}
private fun createDatabase(context: Context) =
Room.databaseBuilder(
context.applicationContext,
ArticleDatabase::class.java,
"article_db.db"
).build()
}
}
**//DOA**
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(article: Article): Long //returns the id's of the article
**//Entity**
#Entity(
tableName = "articles", indices = [Index(value = ["url","title"], unique = true)]
)
#Parcelize
data class Article(
#PrimaryKey(autoGenerate = true)
var id: Int? =null,
val author: String?,
val description: String?,
val source: Source?,
val title: String?,
val url: String?,
val urlToImage: String?,
val publishedAt: String?,
val content: String?
): Parcelable {
override fun hashCode(): Int {
var result = id.hashCode()
if(url.isNullOrEmpty()){
result = 31 * result + url.hashCode()
}
return result
}
}
Expecting the top row to be updated instead of the bottom row, the Room Database docs did not talk much about changing the way the article is inserted, tried to reference other people's work, couldn't find the solution

Problem in TypeConverters In Room Database

I am trying to use type converters in Android (Kotlin) so i am using the type converters class but i am getting confused like inside of the clouds i am having a single variable so i have returned it but
#Entity(tableName = "WeatherDb")
data class WeatherDTO(
val base: String,
val clouds: Clouds,
val cod: Int,
val coord: Coord,
val dt: Int,
#PrimaryKey(autoGenerate = true)
val id: Int,
val main: Main,
val name: String,
val sys: Sys,
val timezone: Int,
val visibility: Int,
val weather: List<Weather>,
val wind: Wind
)
class TypeConverters {
#TypeConverter
fun fromCloudsToDouble(clouds: Clouds): Int {
return clouds.all
}
fun fromCoordToDouble(coord: Coord): Double {
}
}
In coord class here are multiple with different datatypes how to covert this?
data class Main(
val feels_like: Double,
val grnd_level: Int,
val humidity: Int,
val pressure: Int,
val sea_level: Int,
val temp: Double,
val temp_max: Double,
val temp_min: Double
)
Clouds.kt
data class Clouds(
val all: Int
)
Coord.kt
data class Coord(
val lat: Double,
val lon: Double
)
Main.kt
data class Main(
val feels_like: Double,
val grnd_level: Int,
val humidity: Int,
val pressure: Int,
val sea_level: Int,
val temp: Double,
val temp_max: Double,
val temp_min: Double
)
Sys.kt
data class Sys(
val country: String,
val id: Int,
val sunrise: Int,
val sunset: Int,
val type: Int
)
Weather.kt
data class Weather(
val description: String,
val icon: String,
val id: Int,
val main: String
)
Wind.kt
data class Wind(
val deg: Int,
val gust: Double,
val speed: Double
)
WeatherViewModel.kt
#HiltViewModel
class WeatherViewModel #Inject constructor(
private val repo:WeatherRepository,
private val application: Application,
private val WeatherDb:WeatherDB,
private val fusedLocationProviderClient: FusedLocationProviderClient
) :ViewModel(){
private val _resp = MutableLiveData<WeatherDTO>()
val weatherResp:LiveData<WeatherDTO>
get() = _resp
private val _cord = MutableLiveData<Coord>()
val cord:LiveData<Coord>
get() = _cord
var locality:String = ""
fun getWeather(latitude:Double,longitude:Double) =
viewModelScope.launch {
repo.getWeather(latitude,longitude).let { response->
if(response.isSuccessful){
Log.d("response","${response.body()}")
WeatherDb.WeatherDao().insertWeather(response.body()!!)
_resp.postValue(response.body())
}else{
Log.d("Weather Error","getWeather Error Response: ${response.message()}")
}
}
}
fun fetchLocation():Boolean{
val task = fusedLocationProviderClient.lastLocation
if(ActivityCompat.checkSelfPermission(application,android.Manifest.permission.ACCESS_FINE_LOCATION)
!=PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(application,android.Manifest.permission.ACCESS_COARSE_LOCATION)
!=PackageManager.PERMISSION_GRANTED
){
return true
}
task.addOnSuccessListener {
if(it!=null){
getWeather(it.latitude,it.longitude)
getAddressName(it.latitude,it.longitude)
Log.d("localityname", locality)
}
}
return true
}
private fun fetchLocationDetails(){
}
private fun getAddressName(lat:Double,long:Double):String{
var addressName = " "
val geoCoder = Geocoder(application, Locale.getDefault())
val address = geoCoder.getFromLocation(lat,long,1)
if (address != null) {
addressName = address[0].adminArea
}
locality = addressName
Log.d("subadmin",addressName.toString())
Log.d("Address", addressName)
return addressName
}
fun getCoordinates(cord:String){
val geocoder = Geocoder(application,Locale.getDefault())
val address = geocoder.getFromLocationName(cord,2)
val result = address?.get(0)
if (result != null) {
getWeather(result.latitude,result.longitude)
getAddressName(result.latitude,result.longitude)
}
}
}
Here is my converter in the Kotlin:
class Converters {
#TypeConverter
fun valueFromDomainToStorage(value: Value): String {
return value.convertToJson()
}
#TypeConverter
fun valueFromStorageToDomain(str: String): Value {
// we can not create an empty instance of value as TypeDecoder.java should call non-empty constructor
return Value(
"just a stub",
BigInteger.valueOf(0),
BigInteger.valueOf(0),
false,
BigInteger.valueOf(0)
)
.fromJson(str)
}
}
where .convertToJson() and .fromJson(str) implemented as extensions within Value class:
fun Value.convertToJson(): String {
val result = JSONObject()
result.put(ValueConst.OFFER_FIELD, offer)
result.put(ValueConst.AVAILABLE_SINCE, availableSince.toLong())
result.put(ValueConst.AVAILABLE_END, availabilityEnd.toLong())
result.put(ValueConst.IS_CONSUMED, isConsumed)
result.put(ValueConst.LOCKED_UNTIL, lockedUntil)
return result.toString()
}
fun Value.fromJson(json: String): Value {
val subj = JSONObject(json)
return Value(
subj.optString(ValueConst.OFFER_FIELD),
BigInteger.valueOf(subj.optLong(ValueConst.AVAILABLE_SINCE)),
BigInteger.valueOf(subj.optLong(ValueConst.AVAILABLE_END)),
subj.optBoolean(ValueConst.IS_CONSUMED),
BigInteger.valueOf(subj.optLong(ValueConst.LOCKED_UNTIL))
)
}
You should implement Converter class for each non-native class type. Do not forget to register your converters on database:
#Database(entities = [ChainTransaction::class], version = 1, exportSchema = false)
#TypeConverters(Converters::class)
abstract class AppDatabase: RoomDatabase() {
When you have compile the code and later introduce new changes, you have to increase version parameter too to make changes to take effect:
#Database(entities = [ChainTransaction::class], version = 2, exportSchema = false)
#TypeConverters(Converters::class)
abstract class AppDatabase: RoomDatabase() {
Here is official documentation and even training on this topic:
https://developer.android.com/training/data-storage/room
so i am using the type converters class but i am getting confused
SQLite (the database around which Room is an object orientated wrapper) is not an object orientated (or aware) database. It is a database that can store primitive types of data which are one of
INTEGER (such as Int or Long), REAL
REAL (such as Float or Double)
TEXT (such as String)
BLOB (such as ByteArray)
NULL
Therefore to store a type of Coord, Cloud or Weather .... you have three options:-
to embed the class, in which case the fields are copied from the embedded class (would be complicated if the embedded classes contained unsupported types). not covered in the answer
to have the class as a table in it's own right with a relationship between it and the parent (WeatherDTO). not covered in the answer
to convert the class to one of the SQLite types (of which either TEXT or BLOB would probably only be practical).
Considering option 3 (TyepConverters) converting the data is of little, if any, use just storing the data as you would not be able to retrieve the data.
As such type converters should always be paired.
One of the pair will be to convert from the class to a type that can be stored.
The other will be to convert from the stored type to the class.
As such you will need quite a few type Converters, that is 2 each for fields:-
clouds (class Clouds)
coord (class Coord)
main (class Main)
sys (class Sys)
weather (class List)
wind (class Wind)
It is the Class of the field that Room looks at to locate the respective type converter.
One of the simplest ways to convert objects (aka classes) is to convert the object to a JSON representation. Although a complexity with this is that there are many JSON libraries and they will often have differences.
For the examples that follow Googles JSON library has been used. However, use of this library with Room doesn't appear to directly support the use of List<the_class> e.g. List.
The dependency for this being (as an example) implementation 'com.google.code.gson:gson:2.10'
As a get around a new class WeatherList has ben used as per:-
data class WeatherList(
val weatherList: List<Weather>
)
and the WeatherDTO class has been changed to use it as per :-
....
//val weather: List<Weather>,
val weather: WeatherList,
....
As such the TypeConverters class could then be:-
class TypeConverters {
#TypeConverter
fun fromCloudsToJSONString(clouds: Clouds): String = Gson().toJson(clouds)
#TypeConverter
fun toCloudsFromJSONString(jsonString: String): Clouds = Gson().fromJson(jsonString,Clouds::class.java)
#TypeConverter
fun fromCoordToJSONString(coord: Coord): String = Gson().toJson(coord)
#TypeConverter
fun toCoordFromJSONString(jsonString: String): Coord = Gson().fromJson(jsonString,Coord::class.java)
#TypeConverter
fun fromMaintoJSONString(main: Main): String = Gson().toJson(main)
#TypeConverter
fun toMainFromJSONString(jsonString: String): Main = Gson().fromJson(jsonString,Main::class.java)
#TypeConverter
fun fromSysToJSONString(sys: Sys): String = Gson().toJson(sys)
#TypeConverter
fun toSysFromJSONString(jsonString: String): Sys = Gson().fromJson(jsonString,Sys::class.java)
#TypeConverter
fun fromWeatherListFromJSONString(weatherList: WeatherList): String = Gson().toJson(weatherList)
#TypeConverter
fun toWeatherListFromJSOnString(jsonString: String): WeatherList = Gson().fromJson(jsonString,WeatherList::class.java)
#TypeConverter
fun fromWindToJSONString(wind: Wind): String = Gson().toJson(wind)
#TypeConverter
fun toWindFromJSONString(jsonString: String): Wind = Gson().fromJson(jsonString,Wind::class.java)
}
As such the all the types/classes/objects that are not directly supported are converted to/from a JSON string representation of the type/class/object.
Note that you need to add the #TypeConverters(#TypeConverters( value = [<????>.TypeConverters::class]). Where has to distinguish between your projects TypeConverters class from Room's (TypeConverters is probably not the best name for the class, renaming it, would overcome the need to distinguish)
Working Example
The following puts the above into action.
As the question does not include the underlying classes, the following have been used:-
data class Coord(
val longitude: Double,
val latitude: Double
)
data class Clouds(
val cover: Double,
val type: String
)
data class Main(
val main: Double
)
data class Sys(
val sys: Double
)
data class WeatherList(
val weatherList: List<Weather>
)
data class Weather(
val weather: Double
)
data class Wind(
val wind: Double
)
The #Dao annotated interface was also made up and is simply:-
#Dao
interface AllDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(weatherDTO: WeatherDTO)
#Query("SELECT * FROM weatherdb")
fun getAllFromWeatherDB(): List<WeatherDTO>
}
Also the #Database annotated abstract class was made up it being:-
#TypeConverters( value = [a.a.so74384736typeconverterconfusion.TypeConverters::class])
#Database(entities = [WeatherDTO::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDao(): AllDao
companion object {
private var instance: TheDatabase? = null
fun getInstance(context: Context): TheDatabase {
if (instance==null) {
instance = Room.databaseBuilder(context,TheDatabase::class.java,"the_database.db")
.allowMainThreadQueries()
.build()
}
return instance as TheDatabase
}
}
}
Note the package name used to distinguish the TypeConverters class from Room's TypeConverters class
the package name cannot be used elsewhere, so if the above is copied then it would have to be changed. There is no expectation that the code in it's entirety would be copied and used. The code is designed solely to demonstrate the TypeConverters.
Last some activity code to actually do something (store and retrieve some data):-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getAllDao()
dao.insert(
WeatherDTO(
"base001",
Clouds(25.5,"cumulus"),10,
Coord(10.567,30.345),
11,
12,
Main(12345.67890),
"thename",
Sys(9.87654321),
14,
1000,
WeatherList(listOf(Weather(5.1234),Weather(6.5432), Weather(7.6543))),
Wind(23.12)
)
)
for (wdto in dao.getAllFromWeatherDB()) {
Log.d("DBINFO","base = ${wdto.base} longitude = ${wdto.coord.longitude} latitude = ${wdto.coord.latitude} etc ....")
}
}
}
RESULT
When run the log contains, as expected:-
D/DBINFO: base = base001 longitude = 10.567 latitude = 30.345 etc ....
Using App Inspection then the database looks like:-
The fields converted to a JSON string have been highlighted.
Obviously the data will very likely not exactly be as you would expect due to the made up classes.
Follow on from the previous answer #Embedded versus Type Converters
As can be seen from the previous answer, there are some issues in regard to using TypeConverters. From a database perspective the TypeConverters will inevitably contain bloat/unecessary data which is contrary to normalisation (not needlessly storing repetitive data).
As an example the JSON representation will for every row contain exactly the same the field names wasting storage, all rows will have the additional overhead of storing the delimiters ([s and ]s, {s and }s, :s ,s). Furthermore actually using the stored data can become complex due to the bloat and also due to multiple values being stored in a single column and as such can be restrictive.
It would be more efficient to not store the bloat and it could eliminate complexities and enhance the usability of the stored data from a database perspective (querying the data for retrieval) to not store multiple values in a single column.
Using the #Embedded annotation can very easily eliminate the bloat. Consider the following (an alternative version of the WeatherDTO class/entity):-
#Entity(tableName = "WeatherDbAlternative1")
data class WeatherDTOAlternative1(
val base: String,
#Embedded
val clouds: Clouds,
val cod: Int,
#Embedded
val coord: Coord,
val dt: Int,
#PrimaryKey(autoGenerate = true)
val id: Int,
#Embedded
val main: Main,
val name: String,
#Embedded
val sys: Sys,
val timezone: Int,
val visibility: Int,
//val weather: List<Weather>,
/* Unable to embed directly so not embedding */
val weather: WeatherList,
#Embedded
val wind: Wind
)
Bar the weather field all that has been done is add the #Embedded annotation. Noting that the classes of the fields all have fields of types directly supported by Room.
Adding this entity to the #Database annotation and adding a couple of additional functions in the #Dao annotated class as per:-
#Query("SELECT * FROM weatherdbalternative1")
fun getAllFromWeatherDBAlternative1(): List<WeatherDTOAlternative1>
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(weatherDTOAlternative1: WeatherDTOAlternative1)
And then amending the Activity code to include :-
/*ALTERNATIVE 1 All but WeatherList embedded */
dao.insert(
WeatherDTOAlternative1(
"base001A",
Clouds(25.5, "cumulus"),
10,
Coord(10.567, 30.345),
11,
12,
Main(12345.67890),
"thenameA1",
Sys(9.87654321),
14,
1000,
WeatherList(listOf(Weather(5.1234), Weather(6.5432), Weather(7.6543))),
Wind(23.12)
)
)
for (wdto in dao.getAllFromWeatherDBAlternative1()) {
Log.d(
"DBINFO",
"base = ${wdto.base} longitude = ${wdto.coord.longitude} latitude = ${wdto.coord.latitude} etc ...."
)
}
Now results in the Log including:-
D/DBINFO: base = base001 longitude = 10.567 latitude = 30.345 etc ....
D/DBINFO: base = base001A longitude = 10.567 latitude = 30.345 etc ....
i.e. effectively the same data is stored and retrievable
However the data is now stored in the database as (ignoring the weather field) as :-
i.e. the data stored is much cleaner but at the expense of additional columns (which can be advantageous).
additionally although not apparent, the fields that have the #Embedded annotation do not need the TypeConverters.

Android Room - ColumnInfo Cannot find getter for field. private final ... = null;

error: Cannot find getter for field.
private final com.kbb.webviewolacakmi.model.content icerik = null;
I didn't manage to add the subparts of the json to the room.
Thanks to everyone who helped.
I would be very happy if you could write a clear code example.
Json File :
{
"date": "xxx",
"title": {
"rendered": "Title"
},
"content": {
"rendered": "content",
"protected": false
},
}
Data Class :
#Entity
data class Icerik(
#ColumnInfo(name="title")
#SerializedName("title")
val baslik:title?,
#ColumnInfo(name="content")
#SerializedName("content")
public val icerik:content?,
#ColumnInfo(name="date")
#SerializedName("date")
val tarih:String?,
#ColumnInfo(name="jetpack_featured_media_url")
#SerializedName("jetpack_featured_media_url")
val gorsel:String?,) {
#PrimaryKey(autoGenerate = true)
var uuid:Int=0
fun getIcerik(){
}
}
data class content(
#ColumnInfo(name="rendered")
#SerializedName("rendered")
public val content: String?,
#ColumnInfo(name="protected")
#SerializedName("protected")
val bool: Boolean?,
){
#PrimaryKey(autoGenerate = true)
var uuid:Int=0
}
data class title(
#ColumnInfo(name="rendered")
#SerializedName("rendered")
val ytitle:String?
){
#PrimaryKey(autoGenerate = true)
var uuid:Int=0
}
IcerikDatabase Class
#TypeConverters(value = [RoomTypeConverters::class])
#Database(entities = arrayOf(Icerik::class), version = 1)
abstract class IcerikDatabase:RoomDatabase() {
abstract fun icerikDao(): IcerikDAO
companion object {
#Volatile private var instance:IcerikDatabase? = null
private val lock=Any()
operator fun invoke(context: Context)= instance?: synchronized(lock){
instance?: databaseOlustur(context).also {
instance=it
}
}
private fun databaseOlustur(context: Context) = Room.databaseBuilder(
context.applicationContext, IcerikDatabase::class.java,
"icerikdatabase"
).build()
}
}
IcerikDao
interface IcerikDAO {
#Insert
suspend fun instertAll(vararg icerik:Icerik):List<Long>
#Query("SELECT * FROM icerik")
suspend fun getAllIcerik():List<Icerik>
#Query("SELECT * FROM icerik WHERE uuid=:icerikId ")
suspend fun getIcerik(icerikId:Int):Icerik
#Query("DELETE FROM icerik")
suspend fun deleteAllIcerik()
}
TypeConverter
class RoomTypeConverters {
#TypeConverter
fun fromTitleToJSONString(title: title?): String? {
return Gson().toJson(title)
}
#TypeConverter
fun toTitleFromJSONString(jsonString: String?): title? {
return Gson().fromJson(jsonString, title::class.java)
}
#TypeConverter
fun fromIcerikToJSONString(content: content?): String? {
return Gson().toJson(content)
}
#TypeConverter
fun toIcrerikFromJSONString(jsonString: String?): content? {
return Gson().fromJson(jsonString, content::class.java)
}
}
I believe that your issue is in regard, not to room, but with the JSON handling.
The JSON file that you have shown cannot directly build an Icerik object.
Rather you need to have an intermediate class that can be built with the JSON and then use that intermediate class to then build the IceRik object.
So you want an intermediate class something along the lines of:-
data class JsonIceRik(
val content: content,
val title: title,
val date: String
)
If the JSON is then amended to be:-
val myjson = "{\"date\": \"xxx\",\"title\": {\"rendered\": \"Title\"},\"content\": {\"rendered\": \"content\",\"protected\": false}}"
note the omission of the comma between the two closing braces
Then you could use:-
val m5 = Gson().fromJson(myjson,JsonIceRik::class.java)
To build the intermediate JsonIceRik object.
And then you could use:-
val i5 = Icerik(baslik = m5.title, icerik = m5.content, tarih = m5.date,gorsel = "whatever")
To build the Icerik from the intermediate JsonIceRik.
The result in the database would be:-
uuid in the title and content serve no purpose and will always be 0 if obtaining the data from JSON
the #PrimaryKey annotation only serves to introduce warnings -
A Table can only have 1 Primary Key ( a composite Primary Key can include multiple columns though (but you cannot use the #PrimaryKey annotation, you have to instead use the primarykeys parameter of the #Entity annotation) )
You might as well have :-
data class content(
#ColumnInfo(name="rendered")
#SerializedName("rendered")
val content: String?,
#ColumnInfo(name="protected")
#SerializedName("protected")
val bool: Boolean?,
)
data class title(
#ColumnInfo(name="rendered")
#SerializedName("rendered")
val ytitle:String?
)
Otherwise, as you can see, the data according to the JSON has been correctly stored.

Kotlin Room Database Save Control

I save data to Room database with a button. However, every time I press the button, that item is registered and listed again and again. I want to write a control. I do not want it to save it if it is registered. I will be glad if you help.
ArticleDao
#Dao
interface ArticleDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(article: Article): Long
NewsRepisotry
class NewsRepository(
val db: ArticleDatabase
) {
suspend fun upsert(article: Article)=db.getArticleDao().upsert(article)
fun getSavedNews()=db.getArticleDao().getAllArticles()
ArticleDatabase
#Database(
entities = [Article::class],
version = 1
)
#TypeConverters(Converters::class)
abstract class ArticleDatabase : RoomDatabase() {
abstract fun getArticleDao(): ArticleDao
companion object {
#Volatile
private var instance: ArticleDatabase? = null
private val LOCK = Any()
operator fun invoke(context: Context) = instance ?: synchronized(LOCK) {
instance ?: createDatabase(context).also { instance = it }
}
private fun createDatabase(context: Context) =
Room.databaseBuilder(
context.applicationContext,
ArticleDatabase::class.java,
"article_db.db"
).build()
}
}
Article.kt
#Entity(
tableName = "articles"
)
data class Article(
#PrimaryKey(autoGenerate = true)
var Id: Int? = null,
val author: String?,
val content: String?,
val description: String?,
val publishedAt: String?,
val source: Source?,
val title: String?,
val url: String?,
val urlToImage: String?
) : Serializable
Your issue would appear to be that #Insert(onConflict = OnConflictStrategy.REPLACE) is inserting instead of replacing.
The OnConflict strategy will only be actioned if there is a conflict. That is typically that a UNIQUE constraint is violated (see link below for other violations covered).
From your explanation of the issue there is no violation and thus instead of the row(s) being replaced, they are instead inserted.
You may wish to refere to https://sqlite.org/lang_conflict.html
In short you need a UNIQUE index to cover your scenario to ensure that a violation occurs.
Without the Article class it is impossible to say what actual change is required. However the following example shows 2 classes (entities) and how a unique index can be added to a column/property articleName.
So first a guess as what your Article class could be:-
#Entity
data class Article(
#PrimaryKey
var articelId: Long? = null,
var articleName: String,
var otherData: String
)
and here an equivalent with a UNIQUE index on the articleName property/column :-
#Entity(indices = [Index(value = ["articleName"],unique = true)])
data class ArticleV2(
#PrimaryKey
var articleId: Long? = null,
var articleName: String,
var otherData: String
)
Example/Demo
As an example of how both work differently, the latter according to your expectations, consider the following code, utilising the above, (for brevity/convenience run on the main thread) :-
The #Dao ArticleDao for both tables/entiities/classes :-
#Dao
interface ArticleDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun upsert(article: Article): Long
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun upsert(articleV2: ArticleV2)
#Query("SELECT count(*) FROM article")
fun getArticleCount(): Long
#Query("SELECT count(*) FROM articlev2")
fun getArticleV2Count(): Long
}
The #Database ArticleDatabase class :-
#Database(
entities = [Article::class, ArticleV2::class],
version = 1
)
abstract class ArticleDatabase : RoomDatabase() {
abstract fun getArticleDao(): ArticleDao
companion object {
#Volatile
private var instance: ArticleDatabase? = null
private val LOCK = Any()
operator fun invoke(context: Context) = instance ?: synchronized(LOCK) {
instance ?: createDatabase(context).also { instance = it }
}
private fun createDatabase(context: Context) =
Room.databaseBuilder(
context.applicationContext,
ArticleDatabase::class.java,
"article_db.db"
)
.allowMainThreadQueries()
.build()
}
}
Finally an activity, that replicates your "every time I press the button" by looping 5 times inserting one row into each table per iteration and then after the loop writing the number of rows in each of the tables to the log.
class MainActivity : AppCompatActivity() {
lateinit var db: ArticleDatabase
lateinit var dao: ArticleDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = ArticleDatabase.invoke(this)
dao = db.getArticleDao()
for(i in 1..5) {
dao.upsert(Article(null, "Article", "otherdata$i"))
dao.upsert(ArticleV2(null,"Article","otherdata$i"))
}
Log.d("ARTICLECOUNT","Number of Articles in article table = " + dao.getArticleCount() + "... in articleV2 table = " + dao.getArticleV2Count())
}
}
When run (first time) the output in the log contains:-
D/ARTICLECOUNT: Number of Articles in article table = 5... in articleV2 table = 1
So 5 rows inserted into the article table but only 1 row in the articleV2 table (as the value used for the articleName is the same).
After running Database Inspector shows :-
The article table
The articleV2 table
As can be seen the otherData column has been replaced throughout and is otherdata5
If the OnConflict strategy were IGNORE then otherData would have a value of otherdata1
Additional
regarding the comment and Article class now added.
First of all, thank you sir. I have also added the Article class. Your explanation is very good, but I could not understand exactly what I needed to change.
You need to decide what constitutes a duplicate entry.
Perhaps the URL if unchanged could be the most likely to indicate a duplicate.
So you could use :-
#Entity( tableName = "articles", indices = [Index(value = ["url"],unique = true)] ) //<<<<<<<<< ONLY CHANGE IS THE #Entity
data class Article(
#PrimaryKey(autoGenerate = true)
var Id: Int? = null,
val author: String?,
val content: String?,
val description: String?,
val publishedAt: String?,
val source: Source?,
val title: String?,
val url: String?,
val urlToImage: String?
) : Serializable
thus a UNIQUE index (one that does not allow the same value to be stored more than once) would be created and maintained on the url column.
Note NULL is not considered a duplicate value (that is a NULL is considered to be different to any other NULL), so if this value can be null then perhaps it is not a wise choice.
However, you may decide that multiple columns should constitute a duplicate to not be inserted. Here's a variation showing how multiple columns (composite index) can be used.
#Entity( tableName = "articles", indices = [Index(value = ["author","content","description"],unique = true)] ) //<<<<<<<<< ONLY CHANGE IS THE #Entity
data class Article(
#PrimaryKey(autoGenerate = true)
var Id: Int? = null,
val author: String?,
val content: String?,
val description: String?,
val publishedAt: String?,
val source: Source?,
val title: String?,
val url: String?,
val urlToImage: String?
) : Serializable
in this case if the author, content and description were the same then it would be a duplicate.
Note in all cases allowing all null values (? after all) could be problematic if there is no validation to stop all values being null (i.e. the data would probably be useless).
You may also wish to consider clearing inputs (some only perhaps) after an insertion, thus making it less likely for a duplicate to be attemtped.

no such table:#DatabseView

I was trying to create a database view in android but getting this error:
error: There is a problem with the query: [SQLITE_ERROR] SQL error or
missing database (no such table: ChatsView)
public abstract androidx.lifecycle.LiveData>
getMessageViewLIst();
Please help me with this that how can i get data from a DatabaseView in room persistence android
DatabaseView Class
#DatabaseView("SELECT chats.username as username," +
"(SELECT chat.message FROM ChatEntityModel as chat WHERE chat.username=chats.username ORDER BY id DESC LIMIT 1) as lastMsg" +
"(SELECT chat.timeStamp FROM ChatEntityModel as chat WHERE chat.username=chats.username ORDER BY id DESC LIMIT 1) as lastMsgTime " +
"(SELECT count(1) FROM ChatEntityModel as chat WHERE chat.username=chats.username and status=4 ) as totalNewMsgCount From ChatEntityModel as chats",viewName = "ChatsView")
data class ChatsView (
val username:String,
val lastMsg:String,
val lastMsgTime:String,
val totalNewMsgCount:String
)
Entity Class
#Entity(tableName = "ChatEntityModel")
data class ChatEntityModel (
#ColumnInfo(name = "message") var KEY_MESSAGE: String,
#ColumnInfo(name = "username") var KEY_USERNAME : String,
#ColumnInfo(name = "msgsend") var KEY_MSG_SEND : Boolean,
#ColumnInfo(name = "timeStamp") var KEY_TIMESTAMP :String,
#ColumnInfo(name = "status") var KEY_STATUS :Int,
#ColumnInfo(name = "msgid") var KEY_MSG_ID :String,
#ColumnInfo(name = "timestamp_delivered") var KEY_TIME_DELIVERED :String,
#ColumnInfo(name = "timestamp_read") var KEY_TIME_READ :String,
#ColumnInfo(name = "progress") var KEY_PROGRESS :String,
#ColumnInfo(name = "type") var KEY_TYPE : String
){
#PrimaryKey(autoGenerate = true)#ColumnInfo(name = "id")var KEY_ID:Int=0
}
DaoAccess class
#Database(entities = [(ChatEntityModel::class)], views = [(ChatsView::class)], version = 1,
exportSchema = false)
abstract class DaoDatabaseAccess:RoomDatabase() {
abstract fun ChattingDao():ChattingDao
// abstract fun ChattViewDao():ChattViewDao
}
Dao
#Dao
interface ChattingDao {
#Insert(onConflict = OnConflictStrategy.ABORT)
fun insertChatList(chats: List<ChatEntityModel>)
#Insert(onConflict = OnConflictStrategy.ABORT)
fun inserChat(chats: ChatEntityModel)
#Update
fun updateMovie(chats: ChatEntityModel)
#Query("SELECT * FROM ChatEntityModel WHERE id = :id_")
fun getMovie(id_: Int): ChatEntityModel
#Query("SELECT * FROM ChatEntityModel WHERE username=:Jid ORDER BY id ASC")
fun getChatList(Jid:String?): LiveData<List<ChatEntityModel>>
#Query("Update ChatEntityModel SET status=2 , timestamp_delivered=:timeDelivered WHERE msgid=:msg_id
and status <> 3 and username=:Jid ")
fun setChatDelivered(timeDelivered: String?,msg_id:String?,Jid: String?)
#Query("SELECT * FROM ChatEntityModel WHERE status=0 ORDER BY id LIMIT 1" )
fun getUnsentMessage(): List<ChatEntityModel>
#Query("SELECT msgid FROM ChatEntityModel WHERE status=4 and username=:username ORDER BY id" )
fun getUnReadMessage(username:String): List<String>
#Query("UPDATE ChatEntityModel SET status=1 WHERE msgid= :msgId and status=0")
fun setMessageSent(msgId: String?)
#Query("SELECT * FROM ChatEntityModel WHERE msgid =:msgId")
fun checkIfExists(msgId:String?): List<ChatEntityModel>
#Query("Update ChatEntityModel SET status=3 , timestamp_read=:currentTimeMillis WHERE msgid in (:receiptId) and username=:Jid ")
fun setChatRead(currentTimeMillis: String?, receiptId:List<String>,Jid: String?)
#Query("SELECT * FROM ChatEntityModel WHERE status=4 and username=:Jid ")
fun getUnReadChats(Jid: String?):LiveData<List<ChatEntityModel>>
#Query("UPDATE ChatEntityModel set status=5 WHERE status=4 and msgid in (:chat) ")
fun setChatReceivedRead(chat:List<String>)
#Query("SELECT * FROM ChatsView")
fun getMessageViewLIst(): LiveData<List<ChatsView>>
}
Maybe it's not related to your problem, but maybe it is, sometimes errors are weird. The request of your View as several SELECT which isn't correct, maybe try fixing your Query.
EDIT: For more clarity, the issue is that the Query was wrong and was missing comas.
You need to add the database name to the abstract RoomDatabase class
#Database(entities = [ChatsView::class], version = 1)
abstract class DaoDatabaseAccess:RoomDatabase() {
}

Categories

Resources