Android Jetpack Room One to Many Query Where - android-room

I have two tables of Contacts and Messages
Contacts
#Entity
data class Contacts(
#PrimaryKey
var id: String,
var user: String,
var friend: String,
var recentMessage: String?,
var recentDate: Date,
var createDate: Date
)
2.Messages
#Entity
data class Messages(
#PrimaryKey
var id: String,
var contactsId: String,
var user: String,
var friend: String,
var text: String = "",
var createDate: Date,
)
One-to-one query entity is defined
data class ContactsMessages(
#Embedded val contacts: Contacts,
#Relation(parentColumn = "id", entityColumn = "contactsId")
val messages: Messages?
)
My Dao query method works very well, but only the oldest one can be queried. What if the latest one is queried?
#Transaction
#Query("SELECT * FROM Contacts where user = :user order by recentDate desc")
fun contactsMessageList(vararg user: String): Flow<List<ContactsMessages>>
I can easily change it to one-to-many. If it is one-to-many, how do I sort the side with the more? If I control the number of the side with the more?

I can easily change it to one-to-many. If it is one-to-many, how do I sort the side with the more? If I control the number of the side with the more?
The problem with #Relation is that if you let Room build the result, it does so by getting the parent(s) (the Embedded object) and then it gets ALL children (#Relation objects) via a query that it builds that you have no control over.
One way around this is to get the parent(s) and then get the single child separately. This could be, for example by utilising the following Dao's :-
#Query("SELECT * FROM contacts WHERE user=:user")
abstract fun getContactsByUser(vararg user:String): List<Contacts>
gets the respective Contacts
and :-
#Query("SELECT * FROM messages WHERE contactsId=:contactsId ORDER BY createDate ASC LIMIT 1")
abstract fun orderedMessage(contactsId: String): Messages
gets the respective Message for a single Contacts
and then using the two queries separately using a function for this e.g. :-
fun getOrderedContactsMessages(vararg user: String): List<ContactsMessages> {
var rv = ArrayList<ContactsMessages>()
for(u: String in user) {
for (c: Contacts in getContactsByUser(u))
rv.add(ContactsMessages(c,orderedMessage(c.id)))
}
return rv
}
It should be noted that instead of being in an interface the above is coded in an abstract class and hence the need for abstract fun .... for the functions that would have been abstract if included in an interface.
Working Example
Using the above (noting that for brevity and convenience, run on the main thread, and strings have been used for dates) in conjunction with:-
dao.insert(Contacts("FredID","Fred","Fred's friend","recent message","2021-10-01","2021-01-01"))
dao.insert(Contacts("MaryID","Mary","Mary's friend","recent message","2021-09-30","2021-01-01"))
dao.insert(Contacts("JaneID","Jane","Jane's friend","recent message","2021-09-29","2021-01-01"))
dao.insert(Messages("MSG99","FredID","Fred","This is message 99","2021-10-01","2021-10-01"))
dao.insert(Messages("MSG98","FredID","Fred","This is message 98","2021-09-01","2021-09-01"))
dao.insert(Messages("MSG97","FredID","Fred","This is message 97","2021-08-01","2021-08-01"))
dao.insert(Messages("MSG89","MaryID","Mary","This is message 89","2021-09-30","2021-09-30"))
dao.insert(Messages("MSG88","MaryID","Mary","This is message 88","2021-09-01","2021-09-01"))
dao.insert(Messages("MSG87","MaryID","Mary","This is message 87","2021-08-01","2021-08-01"))
dao.insert(Messages("MSG79","JaneID","Jane","This is message 79","2021-09-29","2021-09-29"))
dao.insert(Messages("MSG78","JaneID","Jane","This is message 78","2021-09-01","2021-09-01"))
dao.insert(Messages("MSG77","JaneID","Jane","This is message 77","2021-08-01","2021-08-01"))
for(cm: ContactsMessages in dao.getOrderedContactsMessages("Fred","Mary")) {
Log.d("MYINFO","ID is ${cm.contacts.id} recent message is ${cm.contacts.recentMessage} ordered message is ${cm.messages!!.text}")
}
Then the result written to the log is :-
D/MYINFO: ID is FredID recent message is recent message ordered message is 2021-08-01
D/MYINFO: ID is MaryID recent message is recent message ordered message is 2021-08-01
if orderedMessage is changed to DESC instead of ASC then :-
D/MYINFO: ID is FredID recent message is recent message ordered message is 2021-10-01
D/MYINFO: ID is MaryID recent message is recent message ordered message is 2021-09-30
efficiency wise, there would be ways to do this via a single query but the SQL would be more complex and it would utilise a different POJO with Messages embedded, which would then require the like named columns to be disambiguated.

Related

Android Room Multimap issue for the same column names

As stated in official documentation, it's preferable to use the Multimap return type for the Android Room database.
With the next very simple example, it's not working correctly!
#Entity
data class User(#PrimaryKey(autoGenerate = true) val _id: Long = 0, val name: String)
#Entity
data class Book(#PrimaryKey(autoGenerate = true) val _id: Long = 0, val bookName: String, val userId: Long)
(I believe a loooot of the developers have the _id primary key in their tables)
Now, in the Dao class:
#Query(
"SELECT * FROM user " +
"JOIN book ON user._id = book.userId"
)
fun allUserBooks(): Flow<Map<User, List<Book>>>
The database tables:
Finally, when I run the above query, here is what I get:
While it should have 2 entries, as there are 2 users in the corresponding table.
PS. I'm using the latest Room version at this point, Version 2.4.0-beta02.
PPS. The issue is in how UserDao_Impl.java is being generated:
all the _id columns have the same index there.
Is there a chance to do something here? (instead of switching to the intermediate data classes).
all the _id columns have the same index there.
Is there a chance to do something here?
Yes, use unique column names e.g.
#Entity
data class User(#PrimaryKey(autoGenerate = true) val userid: Long = 0, val name: String)
#Entity
data class Book(#PrimaryKey(autoGenerate = true) valbookid: Long = 0, val bookName: String, val useridmap: Long)
as used in the example below.
or
#Entity
data class User(#PrimaryKey(autoGenerate = true) #ColumnInfo(name="userid")val _id: Long = 0, val name: String)
#Entity
data class Book(#PrimaryKey(autoGenerate = true) #ColumnInfo(name="bookid")val _id: Long = 0, val bookName: String, val #ColumnInfo(name="userid_map")userId: Long)
Otherwise, as you may have noticed, Room uses the value of the last found column with the duplicated name and the User's _id is the value of the Book's _id column.
Using the above and replicating your data using :-
db = TheDatabase.getInstance(this)
dao = db.getAllDao()
var currentUserId = dao.insert(User(name = "Eugene"))
dao.insert(Book(bookName = "Eugene's book #1", useridmap = currentUserId))
dao.insert(Book(bookName = "Eugene's book #2", useridmap = currentUserId))
dao.insert(Book(bookName = "Eugene's book #3", useridmap = currentUserId))
currentUserId = dao.insert(User(name = "notEugene"))
dao.insert(Book(bookName = "not Eugene's book #4", useridmap = currentUserId))
dao.insert(Book(bookName = "not Eugene's book #5", useridmap = currentUserId))
var mapping = dao.allUserBooks() //<<<<<<<<<< BREAKPOINT HERE
for(m: Map.Entry<User,List<Book>> in mapping) {
}
for convenience and brevity a Flow hasn't been used and the above was run on the main thread.
Then the result is what I believe you are expecting :-
Additional
What if we already have the database structure with a lot of "_id" fields?
Then you have some decisions to make.
You could
do a migration to rename columns to avoid the ambiguous/duplicate column names.
use alternative POJO's in conjunction with changing the extract output column names accordingly
e.g. have :-
data class Alt_User(val userId: Long, val name: String)
and
data class Alt_Book (val bookId: Long, val bookName: String, val user_id: Long)
along with :-
#Query("SELECT user._id AS userId, user.name, book._id AS bookId, bookName, user_id " +
"FROM user JOIN book ON user._id = book.user_id")
fun allUserBooksAlt(): Map<Alt_User, List<Alt_Book>>
so user._id is output with the name as per the Alt_User POJO
other columns output specifically (although you could use * as per allUserBookAlt2)
:-
#Query("SELECT *, user._id AS userId, book._id AS bookId " +
"FROM user JOIN book ON user._id = book.user_id")
fun allUserBooksAlt2(): Map<Alt_User, List<Alt_Book>>
same as allUserBooksAlt but also has the extra columns
you would get a warning warning: The query returns some columns [_id, _id] which are not used by any of [a.a.so70190116kotlinroomambiguouscolumnsfromdocs.Alt_User, a.a.so70190116kotlinroomambiguouscolumnsfromdocs.Alt_Book]. You can use #ColumnInfo annotation on the fields to specify the mapping. You can annotate the method with #RewriteQueriesToDropUnusedColumns to direct Room to rewrite your query to avoid fetching unused columns. You can suppress this warning by annotating the method with #SuppressWarnings(RoomWarnings.CURSOR_MISMATCH). Columns returned by the query: _id, name, _id, bookName, user_id, userId, bookId. public abstract java.util.Map<a.a.so70190116kotlinroomambiguouscolumnsfromdocs.Alt_User, java.util.List<a.a.so70190116kotlinroomambiguouscolumnsfromdocs.Alt_Book>> allUserBooksAlt2();
Due to Note that Room will not rewrite the query if it has multiple columns that have the same name as it does not yet have a way to distinguish which one is necessary. the #RewriteQueriesToDropUnusedColumns doesn't do away with the warning.
if using :-
var mapping = dao.allUserBooksAlt() //<<<<<<<<<< BREAKPOINT HERE
for(m: Map.Entry<Alt_User,List<Alt_Book>> in mapping) {
}
Would result in :-
possibly other options.
However, I'd suggest fixing the issue once and for all by using a migration to rename columns to all have unique names. e.g.

HotChocolate mutation input type uses int instead of ID

I am new to HotChocolate and GraphQL as a whole and am trying to grasp on enabling Nodes and/or Relay support for my GraphQL API. Currently using HotChocolate v12.
Context
I am trying to create a mutation that updates an entity (Client in this example). I am using code-first approach here and have the following:
An input record/class is defined as follows:
public record UpdateClientInput([ID(nameof(Client))] int Id, string Code, string Name, string Subdomain);
The mutation function which returns the payload class:
[UseAppDbContext]
public async Task<UpdateClientPayload> UpdateClientAsync(UpdateClientInput input, [ScopedService] AppDbContext context)
{
var client = await context.Set<Client>().FirstOrDefaultAsync(x => x.Id == input.Id);
// cut for brevity
return new UpdateClientPayload(client);
}
I have enabled support for Nodes by adding this in the services configuration:
builder.Services
.AddGraphQLServer()
.AddQueryType(d => d.Name("Query"))
.AddMutationType(d => d.Name("Mutation"))
// removed others for brevity
.AddGlobalObjectIdentification()
.AddQueryFieldToMutationPayloads();
But yet when I browse the API using Banana Cake Pop, the UpdateClientInput object in the schema definition still uses int instead of the ID! (see screenshot below). So the GraphQL client I am using (Strawberry Shake) generates the input object that does not use the ID! type. Am I missing something here?
So to solve this problem, this was actually a change in HotChocolate v12 where records should use the [property: ID] instead of the [ID] attribute. That change is found here https://chillicream.com/docs/hotchocolate/api-reference/migrate-from-11-to-12#records.
What I did is to change the record declaration from:
public record UpdateClientInput([ID(nameof(Client))] int Id, string Code, string Name, string Subdomain);
to
public record UpdateClientInput([property: ID] int Id, string Code, string Name, string Subdomain);
that generated this input object as:

Use join fetch by default with Spring Data

I have a simple schema "Posts have messages that have users" and I want get messages with User data.
#Entity
class User {
#Id
var id: Int = 0
var name: String = ""
var usenrame: String = ""
}
#Entity
class Messages {
#Id
var id: Int = 0
var text: String = ""
#ManyToOne()
var user = User()
}
class Post {
var id: Int = 0
#OneToMany()
#JoinColum()
var messsages: List<Messages> = ArrayList()
}
This works, but, Spring Data / Hibernate, instead of create a single query with join to get message and user, use two queries
I can use a custom repository method to get directly a message and it works fine
#Query("SELECT m FROM Message m LEFT JOIN FECTH m.user WHERE id =:id")
fun findMessageById(id: Int)
But... I don't get any way to get a Post with all messages and users...
I understand that each Post need a query to get all messages, but, I want that query get too user and not get another query for each message to get the user...
Any ideas?

findByPropertyAndReleation not giving me the expected Entity

I'm importing historical football (or soccer, if you're from the US) data into a Neo4j database using a spring boot application (2.1.6.RELEASE) with the spring-boot-starter-data-neo4j dependency and a standalone, locally running 3.5.6 Neo4j database server.
But for some reason searching for an entity by a simple property and an attached, referenced entity, does not work, althought the relation is present in the database.
This is the part of the model, that is currently giving me a headache:
#NodeEntity(label = "Season")
open class Season(
#Id
#GeneratedValue
var id: Long? = null,
#Index(unique = true)
var name: String,
var seasonNumber: Long,
#Relationship(type = "IN_LEAGUE", direction = Relationship.OUTGOING)
var league: League?,
var start: LocalDate,
var end: LocalDate
)
#NodeEntity(label = "League")
open class League(
#Id
#GeneratedValue
var id: Long? = null,
#Index(unique = true)
var name: String,
#Relationship(type = "BELONGS_TO", direction = Relationship.OUTGOING)
var country: Country?
)
(I left out the Country class, as I'm pretty sure that it is not part of the problem)
To allow running the import more than once, I want to check if the corresponding entity is already present in the database and only import newer ones. So I added the following method SeasonRepository:
open class SeasonRepository : CrudRepository<Season, Long> {
fun findBySeasonNumberAndLeague(number: Long, league: League): Season?
}
But it is giving me a null result instead of the existing entity on consecutive runs, hence I get duplicates in my database.
I would have expected spring-data-neo4j to reduce the passed League to its Id and then have a generated query that looks somewhat like this:
MATCH (s:Season)-[:IN_LEAGUE]->(l:League) WHERE id(l) = {leagueId} AND s.seasonNumber = {seasonNumber} WITH s MATCH (s)-[r]->(o) RETURN s,r,o
but when I turn on finer logging on the neo4j package I see this output in the log file:
MATCH (n:`Season`) WHERE n.`seasonNumber` = { `seasonNumber_0` } AND n.`league` = { `league_1` } WITH n RETURN n,[ [ (n)-[r_i1:`IN_LEAGUE`]->(l1:`League`) | [ r_i1, l1 ] ] ], ID(n) with params {league_1={id=30228, name=1. Bundesliga, country={id=29773, name=Deutschland}}, seasonNumber_0=1}
So for some reason, spring-data seems to think, that the league property is a simple / primitive property and not a full releation, that needs to be resolved by the id (n.league= {league_1}).
I only got it to work, by passing the id of the league, and providing a custom query using the #Query annotation but I actually thought, that it would work with spring-data-neo4j out of the box.
Any help appreciated. Let me know if you need more details.
Spring Data Neo4j does not support objects as parameters at the moment. It is possible to query for properties on related entities/nodes e.g. findBySeasonNumberAndLeagueName if this is a suitable solution.

Spring JPA repoistory findBy IN List - allow null

Short Description
How do I make findBy<Field>In work with IN when the array list input is null. e.g. ignore it. What would your DAO for this look like?
Longer description.
Imagine you have creating a search for users page.
in the application. You have various options to filter on.
created (date range always given)
Country (when null ignore and search all countries)
AgeRange
Job Title
etc...
Now say you want to search for all users in a given date range in a list of countries.
When searching for users I will always search for a date joined however if I have not selected a country I want it to search for all countries.
I am planning on adding several more filter options other than country. So I don't really want to create lots of findBy methods for each possible field combination.
DAO
#Repository
public interface UserDao extends JpaRepository<User, Long> {
public List<BeatRate> findByCreatedBetweenAndCountryIn(Date from, Date to, ArrayList<String> countryList );
}
Test
#Test
public void test() throws ParseException {
Date from = new SimpleDateFormat( "yyyy-MM-dd" ).parse( "2015-01-01" );
Date to = new SimpleDateFormat("yyyy-MM-dd").parse("2015-05-15");
//ArrayList<String> countryList = new ArrayList<String>();
//countryList.add("UK");
//countryList.add("Australia");
//countryList.add("Japan"); // works ok when I have a list
countryList = null; // I want it to search for all countries when this is null -- this errors and doesnt work..
List<BeatRate> beatRates = beatRateDao.findByCreatedBetweenAndRentalCountryIn(from, to, countryList);
Assert.assertTrue(beatRates.size()>0);
}
You can have two methods:
beatRateDao.findByCreatedBetweenAndRentalCountryIn(from, to, countryList);
and
beatRateDao.findByCreatedBetweenAndRental(from, to);
Then simply pick one based on countryList:
List<BeatRate> beatRates = (countryList != null && !countryList.isEmpty())
? beatRateDao.findByCreatedBetweenAndRentalCountryIn(from, to, countryList)
: beatRateDao.findByCreatedBetweenAndRental(from, to);
The IN clause requires a non-nullable and non empty argument list as otherwise the query will fail.
On PostgreSQL, if you try to run a query like this:
select *
from product
where quantity in ( )
you get the following error:
ERROR: syntax error at or near ")"
LINE 3: where quantity in ( )
^
********** Error **********
ERROR: syntax error at or near ")"
SQL state: 42601
Character: 45

Resources