I should save the record in the database and get the record id in the response. After a long search and research I came up with the following option.
data class User(
val id: UUID? = null,
val username: String,
...
)
UserRepo:
#Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
#Insert("""
INSERT INTO "user" (
username,
...
) VALUES (
#{username},
...
)
""")
fun save(user: User): User
in response I get the following.
org.apache.ibatis.binding.BindingException: Mapper method '...UserRepository.save' has an unsupported return type: class ...entity.User
in the following case, I don't get an error, but I don't get an id either, how can I do it correctly? Used Select instead of Insert
#Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
#Select("""
INSERT INTO "user" (
username
) VALUES (
#{username}
)
""")
fun save(user: User): User
if we solve my first question, then I would like to know if it is possible to get the answer not in the User class, but in another one, for example, UserResponse? That is, send a request to the User class, and receive a response in the UserResponse
UPDATE
I was able to get the id after changing the id type to String.
Like this:
data class User(
val id: String? = null,
val username: String,
...
)
Apparently some settings are needed for UUID?
Has anyone faced such a problem?
Related
I have a small project to tinker with Spring, where I have two entities with a one to many association: 1 Restaurant -> N Dishes.
I have the following PostgreSQL schema for that:
create table if not exists restaurants (
restaurant_id uuid primary key,
name varchar(512) not null,
description varchar(1024) not null,
address varchar(512) not null,
photo_url varchar(1024)
);
create table if not exists dishes (
dish_id uuid primary key,
name varchar(512) not null,
description varchar(1024),
photo_url varchar(1024),
restaurant_id uuid references restaurants(restaurant_id) not null,
price int not null check (price > 0)
);
With the following JPA Entities:
#Entity
#Table(name = "restaurants")
class Restaurants(
#Id
var restaurantId: UUID,
var name: String,
var description: String,
var photoUrl: String?,
) {
#OneToMany(mappedBy = "restaurant")
#JoinColumn(name = "restaurant_id", nullable = false)
var dishes: MutableList<Dishes> = mutableListOf()
}
#Entity
#Table(name = "dishes")
class Dishes(
#Id
var dishId: UUID,
var name: String,
var description: String,
var photoUrl: String?,
var price: Int,
#ManyToOne(optional = false)
#JoinColumn(name = "restaurant_id", nullable = false)
var restaurant: Restaurants
)
I have defined a RestaurantsRepository as follows:
interface RestaurantsRepository: R2dbcRepository<Restaurants, UUID> {
fun findByRestaurantId(restaurantId: UUID): Mono<Restaurants>
}
The problem I'm having is that when I call findByRestaurantId I have the following exception:
org.springframework.r2dbc.BadSqlGrammarException: executeMany; bad SQL grammar [SELECT restaurants.restaurant_id, restaurants.name, restaurants.description, restaurants.photo_url, restaurants.dishes FROM restaurants WHERE restaurants.restaurant_id = $1]; nested exception is io.r2dbc.postgresql.ExceptionFactory$PostgresqlBadGrammarException: [42703] column restaurants.dishes does not exist
at org.springframework.r2dbc.connection.ConnectionFactoryUtils.convertR2dbcException(ConnectionFactoryUtils.java:235) ~[spring-r2dbc-5.3.21.jar:5.3.21]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Why is the #OneToMany field included in the SQL query?
You are trying to use Spring Data R2DBC (R2dbcRepository) in conjunction with JPA annotations. It won't work: these are two different technologies. R2DBC does not support #ManyToOne nor #JoinColumn so the annotations are simply ignored.
I have multiple object tyes inside a parent class.Say I have a College class as below
data class College(
#PrimaryKey
val id:String,
val name: String? = null,
val description: String? = null,
val groups: List<Group>? = null,
val status: String? = null,
)
data class Group(
#PrimaryKey
val id:String,
val name: String? = null,
val description: String? = null,
val students: List<Students>? = null,
)
The problem here is there is not relational id in child tables.I mean JSON received don't give relation of groups inside college with collegeId in Groups table or relation of students in group table.
The JSON received is as below
"college": [
{
"id": "collegeid",
"groups": [
"id": "groupid"
"name": "BCOM" // Here no collegeId is mentioned inside it
]
}
If is use #Embedded keyword it is throwing "Entities and POJOs must have a usable public constructor".
Is there anyway with above JSON I can set the id of college inside group and use it as foreign key for relations.
I have used Typeconverters and is working fine but now I need to create relations between these tables with above type of JSON.
I use Gson parsing
I believe that you may be getting mixed up with how to utilise relationships/tables.
IF you have
#Entity
data class College(
#PrimaryKey
val id:String,
val name: String? = null,
val description: String? = null,
val groups: List<Group>? = null,
val status: String? = null,
)
this will be a table where the groups column will (in theory) contain a List of Groups, there is no relationship as there is just a column with a single stream of data (probably as JSON string).
However, if you wanted to have a table with the Colleges, a table with the Groups and a table with the Students then you wouldn't embed the Groups within the College and subsequently the Students within the Groups.
Rather, if you want to approach this from a db relationship aspect. Assuming that a Group MUST belong to one and only one College and that a Student MUST only belong to a single Group then you would have something along the lines of:-
#Entity
data class College(
#PrimaryKey
val id:String,
val name: String? = null,
val description: String? = null,
//val groups: List<Group>? = null, //<<<<< NO NEED (see Group)
val status: String? = null,
)
#Entity
data class Group(
#PrimaryKey
val id:String,
val name: String? = null,
val description: String? = null,
//val students: List<Students>? = null, //<<<<< NO NEED (similar to Group)
val collegeId: String //<<<<< ADDED for relationships to parent College
)
similar with Students
the crucial factor here is the new column/field/variable collegeId, which should contain the id of the parent college.
To get the College(s) with the related groups then you can have a class that has #Embedded annotation for the parent (the College) and #Relation annotation for the children (Groups). e.g.
data class CollegeWithRelatedGroups(
#Embedded
val college: College,
#Relation(
entity = Group::class,
parentColumn = "id",
entityColumn = "collegeId"
)
val groups: List<Group>
)
Working Example
Here's a working example that uses the above and adds (inserts) 3 Colleges, and then 6 Groups. The first College has 3 related Groups, the second 2 and the third 1.
The example then extracts the Colleges with the related Groups outputting the result to the log.
College and also CollegeWithRelatedGroups
#Entity
data class College(
#PrimaryKey
val id:String,
val name: String? = null,
val description: String? = null,
//val groups: List<Group>? = null,
val status: String? = null
)
data class CollegeWithRelatedGroups(
#Embedded
val college: College,
#Relation(
entity = Group::class,
parentColumn = "id",
entityColumn = "collegeId"
)
val groups: List<Group>
)
Group (with Foreign Key constraint)
#Entity(
/* Optional but suggested Foreign Key to enforce referential integrity
*/
foreignKeys = [
ForeignKey(
entity = College::class,
parentColumns = ["id"],
childColumns = ["collegeId"],
/* Optional within Foreign Key - helps maintain referential integrity (if CASCADE)
ON DELETE will delete the Groups that are related to a College if the College is deleted
ON UPDATE will change the collegeId of Groups that are related to a College if the id of the College is changed
*/
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class Group(
#PrimaryKey
val id:String,
val name: String? = null,
val description: String? = null,
//val students: List<Student>? = null,
#ColumnInfo(index = true) /* faster to access via relationship if indexed */
val collegeId: String
)
AllDao
#Dao
interface AllDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(college: College): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(group: Group): Long
#Transaction
#Query("SELECT * FROM college")
fun getCollegesWithRelatedGroups(): List<CollegeWithRelatedGroups>
}
TheDatabase (note for brevity and convenience allows main thread processing)
#Database(entities = [College::class, Group::class], version = 1, exportSchema = false)
abstract class TheDatabase : RoomDatabase() {
abstract fun getAllDao(): AllDao
companion object {
#Volatile
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 there is no need for TypeConverters as objects are not being stored (aka simple types are stored)
Activity Code (MainActivity in this case)
const val TAG = "DBINFO"
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(College("College1","The first College","Opened"))
dao.insert(College("College2","The second College","Opened"))
dao.insert(College("New College","A new College","Being built"))
dao.insert(Group("GroupA","Group A or something","The first Group - hence A","College1",))
dao.insert(Group("GroupB","Group B or whatever"," The second Group ...", "College2"))
dao.insert(Group("GroupC","Group C on so on","The third Group ...","New College"))
dao.insert(Group("GroupX","Group X ...","The Xth group","College1"))
dao.insert(Group("GroupY","...","...","College1"))
dao.insert(Group("GroupZ","...","...","College2"))
for (cwrg in dao.getCollegesWithRelatedGroups()) {
Log.d(TAG,"College is ID=${cwrg.college.id} Desc=${cwrg.college.description} Name=${cwrg.college.name} Status=${cwrg.college.name}\nIt has ${cwrg.groups.size} Groups. They are:-")
for (g in cwrg.groups) {
Log.d(TAG,"\n\t\tGroup is ID=${g.id} Name=${g.name} Desc=${g.description} it reference CollegeID=${g.collegeId}")
}
}
}
}
Note The status has not been supplied for the Colleges. so they will be null.
RESULT the log includes :-
D/DBINFO: College is ID=College1 Desc=Opened Name=The first College Status=The first College
It has 3 Groups. They are:-
D/DBINFO: Group is ID=GroupA Name=Group A or something Desc=The first Group - hence A it reference CollegeID=College1
D/DBINFO: Group is ID=GroupX Name=Group X ... Desc=The Xth group it reference CollegeID=College1
D/DBINFO: Group is ID=GroupY Name=... Desc=... it reference CollegeID=College1
D/DBINFO: College is ID=College2 Desc=Opened Name=The second College Status=The second College
It has 2 Groups. They are:-
D/DBINFO: Group is ID=GroupB Name=Group B or whatever Desc= The second Group ... it reference CollegeID=College2
D/DBINFO: Group is ID=GroupZ Name=... Desc=... it reference CollegeID=College2
D/DBINFO: College is ID=New College Desc=Being built Name=A new College Status=A new College
It has 1 Groups. They are:-
D/DBINFO: Group is ID=GroupC Name=Group C on so on Desc=The third Group ... it reference CollegeID=New College
The Database via App Inspection :-
and
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.
I need to check is user with a specified id and token exists. Tokens are stored in #ElementCollection set. I tried to write a derived query method in my repository like this:
boolean existsByIdAndTokensContains(long id, String token);
But that isn't working (returns false for right input values).
When I tried to specify query explicitly it start working right:
#Query("select (count(u) > 0) from users as u where u.id = :id and :token member of u.tokens")
The most strange that Hibernate logs are looking similar for both queries:
For the derived query:
/* select
generatedAlias0.id
from
users as generatedAlias0
where
(
generatedAlias0.id=:param0
)
and (
:param1 member of generatedAlias0.tokens
) */ select
userentity0_.id as col_0_0_
from
users userentity0_
where
userentity0_.id=?
and (
? in (
select
tokens1_.token
from
users_tokens tokens1_
where
userentity0_.id=tokens1_.user_id
)
) limit ?
For the explicit query:
/* select
(count(u) > 0)
from
users as u
where
u.id = :id
and :token member of u.tokens */ select
count(userentity0_.id)>0 as col_0_0_
from
users userentity0_
where
userentity0_.id=?
and (
? in (
select
tokens1_.token
from
users_tokens tokens1_
where
userentity0_.id=tokens1_.user_id
)
)
And when I tried to query generated query, result contained the right user.
What am I do wrong?
I would think you need to implement an Attribute Converter to convert the id into a Boolean. This way it will know how to map a not null Id to a true value.
Ok, I found the solution.
For my Set of tokens:
#ElementCollection
#Column(name = "token", nullable = false)
#CollectionTable(name = "users_tokens", joinColumns = #JoinColumn(name = "user_id"),
uniqueConstraints = #UniqueConstraint(columnNames = {"user_id", "token"}))
private final Set<String> tokens;
Works the similar query but without Contains:
boolean existsByIdAndTokens(long id, String token);
I don't know is it a bug, but this works for me.
In the SQLite Swift documentation there is reference to getting statement results directly. I have a lot of SQL queries prepared and I don't really want to refactor them. I would sooner use them as they are using db.prepare, as per below.
Statements with results may be iterated over.
let stmt = try db.prepare("SELECT id, email FROM users")
for row in stmt {
print("id: \(row[0]), email: \(row[1])")
// id: Optional(1), email: Optional("alice#mac.com")
}
The return values always have the "Optional()" around them. Is there a way we can just get the raw row values back without this?
Unwrap the values using ! after the variable as #stephencelis said:
let stmt = try db.prepare("SELECT id, email FROM users")
for row in stmt {
print("id: \(row[0]!), email: \(row[1]!)")
}
You may want to use https://github.com/groue/GRDB.swift. It lets you extract optionals or non-optionals, just as you wish:
for row in Row.fetch(db, "SELECT id, email FROM users") {
let id: Int64 = row.value(atIndex: 0)
let email: String = row.value(atIndex: 1)
print(id, email)
}
The type-safe API lets you declare expressions of non-optional types that, when pulled back from a statement, are not wrapped.
From the README:
let users = Table("users")
let id = Expression<Int64>("id")
let name = Expression<String?>("name")
let email = Expression<String>("email")
try db.run(users.create { t in
t.column(id, primaryKey: true)
t.column(name)
t.column(email, unique: true)
})
// CREATE TABLE "users" (
// "id" INTEGER PRIMARY KEY NOT NULL,
// "name" TEXT,
// "email" TEXT NOT NULL UNIQUE
// )
let insert = users.insert(name <- "Alice", email <- "alice#mac.com")
let rowid = try db.run(insert)
// INSERT INTO "users" ("name", "email") VALUES ('Alice', 'alice#mac.com')
for user in db.prepare(users) {
println("id: \(user[id]), name: \(user[name]), email: \(user[email])")
// id: 1, name: Optional("Alice"), email: alice#mac.com
}
Note that both id and email, which are non-optional, are returned as such.