Get LiveData from a many-to-many structure in android Room Architecture Component - android-room

Let's take a basic example
A table to store the users
#Entity (tableName="users")
class UsersEntity(
#PrimaryKey val id
var name:String,
...
)
A table to store the roles
#Entity (tableName="roles")
class RolesEntity(
#PrimaryKey val id
var name:String,
...
)
A table to store the many to many relation between users and roles
#Entity (tableName="roles")
class UserRoles(
#PrimaryKey val id
var userId:String,
var roleId:String
)
The pojo class I need in my View
class user(
var id:String,
var name:String,
.... other fields
var roles: List<Role>
)
In my ViewModel how can I pass a user result as LiveData having also the List<Role> filled?
Looking at a general way, I could:
have UserDao.getUserById(id) which returns LiveData from the users table and RoleDao.getRolesForUserId(id) which returns LiveData with a list of roles for a user. Then in my fragment, I can do viewModel.getUserById().observe{} and viewModel.getRolesForUserId().observe{}. But this basically means having 2 observers and I'm pretty confident that it's not the way to go.
probably other way would be to be able to mix them somehow in my repository or viewmodel so it returns what I need. I'll look into MediatorLiveData

Create a different model with the User and its Roles, and use #Embedded and #Relation annotations.
Take for example:
public class UserModel {
#Embedded
UserEntity user;
#Relation(parentColumn = "id", entityColumn = "userId", entity = UserRoles.class)
List<UserRoleModel> userRoles;
}
public class UserRoleModel {
#Embedded
UserRoles userRole;
#Relation(parentColumn = "roleId", entityColumn = "id")
List<RoleEntity> roles; // Only 1 item, but Room wants it to be a list.
}
You can use the UserModel from here.

It's ok to have multiple different data flow in one screen.
On the one hand we can talk about changing user roles list without changing user itself on the other hand user name can be changed without updating roles list. Addition benefit of using multiple data flow, you can show user data while loading user roles.
I suppose, you have pojo of user and roles to avoid synchronization issues. You can implement smooth data delivering (from db to view) like in sample below:
View model
public class UserRolesViewModel extends ViewModel {
private final MutableLiveData<Integer> mSelectedUser;
private final LiveData<UsersEntity> mUserData;
private final LiveData<List<RolesEntity>> mRolesData;
private DataBase mDatabase;
public UserRolesViewModel() {
mSelectedUser = new MutableLiveData<>();
// create data flow for user and roles synchronized by mSelectedUser
mUserData = Transformations.switchMap(mSelectedUser, mDatabase.getUserDao()::getUser);
mRolesData = Transformations.switchMap(mSelectedUser, mDatabase.getRolesDao()::getUserRoles);
}
public void setDatabase(DataBase dataBase) {
mDatabase = dataBase;
}
#MainThread
public void setSelectedUser(Integer userId) {
if (mDatabase == null)
throw new IllegalStateException("You need setup database before select user");
mSelectedUser.setValue(userId);
}
public LiveData<UsersEntity> getUserData() {
return mUserData;
}
public LiveData<List<RolesEntity>> getRolesData() {
return mRolesData;
}
}
It's better to encapsulate data source implementation in Repository class and inject it via DI like in this paragraph.
Database sample based on Many-to-Many paragraph from this article
Entities
Users
#Entity(tableName = "users")
public class UsersEntity {
#PrimaryKey
public int id;
public String name;
}
Roles
#Entity(tableName = "roles")
public class RolesEntity {
#PrimaryKey
public int id;
public String name;
}
User roles
This entity require special attention because we need to declare foreign keys to make joun operations futire
#Entity(tableName = "user_roles", primaryKeys = {"user_id", "role_id"}, foreignKeys = {
#ForeignKey(entity = UsersEntity.class, parentColumns = "id", childColumns = "user_id"),
#ForeignKey(entity = RolesEntity.class, parentColumns = "id", childColumns = "role_id")
})
public class UserRolesEntity {
#ColumnInfo(name = "user_id")
public int userId;
#ColumnInfo(name = "role_id")
public int roleId;
}
Dao
Users dao
#Dao
public interface UserDao {
#Query("SELECT * from users WHERE id = :userId")
LiveData<UsersEntity> getUser(int userId);
}
Roles dao
#Dao
public interface RolesDao {
#Query("SELECT * FROM roles INNER JOIN user_roles ON roles.id=user_roles.role_id WHERE user_roles.user_id = :userId")
LiveData<List<RolesEntity>> getUserRoles(int userId);
}
Data base
#Database(entities = {UsersEntity.class, RolesEntity.class, UserRolesEntity.class}, version = 1)
public abstract class DataBase extends RoomDatabase {
public abstract UserDao getUserDao();
public abstract RolesDao getRolesDao();
}

Related

Insert into Multiple tables using JPA #Query

Insert JSON values into multiple tables using JPA and spring-boot.
User Table
#Entity
class User {
private #Id #GeneratedValue Long id;
private String name;
#OneToOne(cascade = {
CascadeType.All
})
#JoinColumn(referencedColumnName = "productid")
private Product product;
public User() {}
public User(String name, Product product) {
this.name = name;
this.product = product;
}
}
Product Table
#Entity
class Product {
private #Id #GeneratedValue Long productid;
private String productName;
public Product() {}
public Product(String productName) {
this.productName = productName;
}
}
Repository
#Repository
public interface UserRepo extends JpaRepository < User, Long > {}
Json Input
{
"name": "John",
"product": {
"productName": "Product 1"
}
}
Rest Controller
UserRepo usrRepo;
#PostMapping("/user")
User addEmployee(#RequestBody User user) {
return usrRepo.save(user);
}
When I use the above, both User and Product tables get updated with the new values from JSON. But I want to have the same functionality using #Query. Using the below code, I can update one table but not both.
Help me to insert JSON values into multiple tables using #Query. I am using cockroach db, please suggest if there is any other way to achieve this instead of spring-data-JPA.
Query
#Modifying
#Transactional
#Query(value = "insert into user (name, productid) values (:#{#user.name}, :#{#user.productid})", nativeQuery = true)
void insert(#Param("user) User user);

How to create a get request for many-to-one columns?

I currently have made a spring boot project which is for an event system. In the model for booking class, I have two objects one is an event and the other one is the user. Now I want to create a get request that allows me to get all bookings made by a single user and all the bookings for a single event respectively. I have managed to create the other requests which are getting all the bookings and getting a booking by the booking id.
Right now if I try to make create any sort of implementation it either gives me a null pointer error or tells me the table relation "booking" doesn't exist. Please let me know if it's possible to write such a get request. Thanks
Model:
#Id
#SequenceGenerator(
name = "booking_sequence",
sequenceName = "booking_sequence",
allocationSize = 1
)
#GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "booking_sequence"
)
private Long id;
#ManyToOne
#JoinColumn(
name = "event_id",
referencedColumnName = "id"
)
private Event event;
#ManyToOne
#JoinColumn(
name = "user_id",
referencedColumnName = "id"
)
private User user;
private Integer tickets;
#Transient
private Integer amount;
Repository:
#Repository
public interface BookingRepository extends JpaRepository<Booking, Long > {
#Query
Optional<Booking> findBookingById(Long id);
}
Service:
#Autowired
public BookingService(BookingRepository bookingRepository) {
this.bookingRepository = bookingRepository;
}
public List<Booking> getBookingList() {
return bookingRepository.findAll();
}
public Booking getSingleBooking(Long bookingId) {
return bookingRepository.findBookingById(bookingId).orElseThrow();
}
Controller:
#GetMapping
public List<Booking> getBookings() {
return bookingService.getBookingList();
}
#GetMapping(path = "{bookingId}")
public Booking getSingleBooking(#PathVariable("bookingId") Long bookingId) {
return bookingService.getSingleBooking(bookingId);}
#GetMapping(path = "/user/{userId}")
public List<Booking> getUserBookings(#PathVariable("userId") Long userId) {
return bookingService.getBookingByUser(userId);}
#GetMapping(path = "/event/{eventId}")
public List<Booking> getEventBookings(#PathVariable("eventId") Long eventId) {
return bookingService.getBookingForEvent(eventId);}
you don't need the line
#Query
Optional<Booking> findBookingById(Long id);
the default repository implementation already gives you a findById so you can use it
And #Query can't be used like it used to, you need to pass the query you want, you can find out more here(https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.at-query)
Or you can use this strategy to make your queries https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.sample-app.finders.strategies
So it is possible to make such requests, all I did was use "nativeQuery" so that it would function the way I want it to. As mentioned I wanted to make two get-requests and here is how I wrote the queries for them.
Getting all user bookings of a specific user using its "ID":
#Query(value = "SELECT * from bookings, user where bookings.user_id = :id", nativeQuery = true)
List<Booking> findByUserid(Long id);
Getting all event bookings of a specific event using its "ID":
#Query(value = "SELECT * from bookings, user where bookings.event_id = :id", nativeQuery = true)
List<Booking> findByEventid(Long id);

Spring hibernate ignore json object

I need to remove cart object from json, but only in one controller method and that is:
#GetMapping("/users")
public List<User> getUsers() {
return userRepository.findAll();
}
User
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#NotBlank(message = "Name cannot be empty")
private String name;
#OneToOne
private Cart cart;
}
Cart
#Entity
public class Cart {
#Id
private String id = UUID.randomUUID().toString();
#OneToMany
private List<CartItem> cartItems = new ArrayList<>();
#OneToOne
#JsonIgnore
#OnDelete(action = OnDeleteAction.CASCADE)
private User user;
}
I have done it with simple solution so i loop trough all users, and set their cart to null,and then anotated user entity with #JsonInclude(JsonInclude.Include.NON_NULL)
But i dont think this is propper solution, so im searching for some better solution..
How am i able to do this?
Thanks...
You can create DTO (data transfer object) class like this:
#Data
public class UsersDto {
private Integer id;
private String name;
public UsersDto(User user) {
this.id = user.id;
this.name= user.name;
}
}
and than create List<UsersDto>
#GetMapping("/users")
public List<UsersDto> getUsers() {
List<User> users = userRepository.findAll();
return users
.stream()
.map(o -> new UsersDto(o))
.collect(Collectors.toList());
}
You should use Data Projection.
In your use case, you can use an interface projection:
public interface CartlessUser {
Integer getId();
String getName();
}
And In your repository:
public interface UserRepository extends JpaRepository<User, Integer> {
List<CartlessUser> findAllBy();
}
The interface projection will help generate the sql query for only selecting the id, name fields. This will save you from fetching the Cart data when you're just going to throw it away anyways.

Hibernate creates two tables in a many to many relationship

This is my Product entity class:
public class Product extends BaseEntity {
#Column
#ManyToMany()
private List<Customer> customers = new ArrayList<>();
#ManyToOne
private Supplier supplier;
}
And this is my Customer entity class:
public class Customer extends BaseEntity {
//Enum type to String type in database '_'
#Enumerated(EnumType.STRING)
#Column
private Type type;
#Column
#ManyToMany(targetEntity = Product.class)
private List<Product> products = new ArrayList<>();
}
When I run my Spring boot project, it creates 2 separate tables in my database(Mysql): product_customer and customer_product but I need only one. What can I do to solve this?
Update your classes as follows:
public class Product {
#ManyToMany
#JoinTable(name="product_customer"
joinColumns=#JoinColumn(name="product_id"),
inverseJoinColumns=#JoinColumn(name="customer_id")
)
private List<Customer> customers = new ArrayList<>();
...
}
public class Customer extends BaseEntity {
#ManyToMany
#JoinTable(name="product_customer"
joinColumns=#JoinColumn(name="customer_id"),
inverseJoinColumns=#JoinColumn(name="product_id")
)
private List<Product> products = new ArrayList<>();
...
}
Take a look to the following link to know how to map a ManyToMany relation in a suitable way. But basically, you can do:
public class Product {
...
#ManyToMany(cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
#JoinTable(name="product_customer"
joinColumns=#JoinColumn(name="product_id"),
inverseJoinColumns=#JoinColumn(name="customer_id")
)
private Set<Customer> customers = new LinkedHashSet<>();
...
}
And:
public class Customer extends BaseEntity {
...
#ManyToMany(mappedBy = "customers")
private Set<Product> products = new LinkedHashSet<>();
...
}
As #Kavithakaran mentioned in a comment of his answer, you can use #ManyToMany(mappedBy = ... once you identify the "owner of the relation".
If you mean that you don't want to create the third table then you can read the following link below:-
Hibernate Many to Many without third table
Otherwise, you can do this with #jointable annotation.

Hibernate: ManyToMany inverse Delete

I have a ManyToMany relationship between two tables, User and Keyword. The User is the owner of the relationship. If I delete a User, I remove first all Keywords from this User and then delete the User. This works as expected.
But I don't know how to delete a Keyword and automatically delete the relations to all Users.
Here is my code so far.
#Entity
#Table(name = "user")
public class User {
#Id
#Column(name = "id")
#GeneratedValue
private Integer id;
#Column(name = "name")
private String name;
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#Fetch(value = FetchMode.SUBSELECT)
#JoinTable(name = "user_has_keyword", joinColumns = #JoinColumn(name = "user_id"), inverseJoinColumns = #JoinColumn(name = "keyword_id"))
private List keywords = new ArrayList();
// Getters and setters
...
}
#Entity
#Table(name = "keyword")
public class Keyword {
#Id
#Column(name = "id")
#GeneratedValue
private Integer id;
#Column(name = "keyword")
private String keyword;
#ManyToMany(mappedBy = "keywords")
private List users = new ArrayList();
// Getters and setters
...
}
#Service("myService")
#Transactional("transactionManager")
public class MyService {
#Resource(name = "sessionFactory")
private SessionFactory sessionFactory;
#SuppressWarnings("unchecked")
public List getAllUsers() {
Session session = this.sessionFactory.getCurrentSession();
Query query = session.createQuery("FROM User");
return query.list();
}
public User getUser(Integer id) {
Session session = this.sessionFactory.getCurrentSession();
return (User) session.get(User.class, id);
}
public void addUser(User user) {
Session session = this.sessionFactory.getCurrentSession();
session.save(user);
}
public void deleteUser(User user) {
Session session = this.sessionFactory.getCurrentSession();
// 1st, delete relations
user.getKeywords().clear();
session.update(user);
// 2nd, delete User object
session.delete(user);
}
public Keyword getKeyword(Integer id) {
Session session = this.sessionFactory.getCurrentSession();
return (Keyword) session.get(Keyword.class, id);
}
public Keyword addKeyword(Keyword keyword) {
Session session = this.sessionFactory.getCurrentSession();
session.save(keyword);
return keyword;
}
public void deleteKeyword(Keyword keyword) {
Session session = this.sessionFactory.getCurrentSession();
// 1st, delete relations
keyword.getUsers().clear();
session.update(keyword);
// 2nd, delete User object
keyword = getKeyword(keyword.getId());
session.delete(keyword);
}
}
#Controller
public class MyController {
#Resource(name = "myService")
private MyService myService;
#RequestMapping(value = "/add", method = RequestMethod.GET)
public String add(Model model) {
Keyword k = new Keyword();
k.setKeyword("yellow");
k = myService.addKeyword(k);
User u1 = new User();
u1.setName("Bart");
u1.getKeywords().add(k);
myService.addUser(u1);
User u2 = new User();
u2.setName("Lisa");
u2.getKeywords().add(k);
myService.addUser(u2);
return "/";
}
#RequestMapping(value = "/delete/user", method = RequestMethod.GET)
public String deleteUser(Model model) {
User u = myService.getUser(1);
myService.deleteUser(u);
return "/";
}
#RequestMapping(value = "/delete/keyword", method = RequestMethod.GET)
public String deleteKeyword(Model model) {
Keyword k = myService.getKeyword(1);
myService.deleteKeyword(k);
return "/";
}
}
If I browse to /delete/keyword I get the following exception:
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.blabla.prototype.Keyword.users, no session or session was closed
I've googled and tried many different things, but nothing works.
I appreciate any help.
Thank you very much,
Marco
The LazyInitializationException has nothing to do with deletion. You're loading a Keyword in your controller. This makes the service load the keyword, without initializing its lazy list of users. This keyword is then returned to the controller, and the transaction is committed, and the session is closed, making the keyword detached from the session.
Then you pass this detached keyword to the service to delete it. The service thus receives a detached keyword, and tries to access its list of users. Since the keyword is detached and the list of users hasn't been loaded yet, this causes a LazyInitializationException.
The service method should take the ID of the keyword to delete as argument, load it, and thus work with an attached keyword, and then proceed with the deletion.
Now to answer your question, you got it right for the user deletion: you remove all the keywords from the user to delete, because the user is the owner of the association. Apply the same logic when deleting a keyword : remove the keyword from all the users referencing it, and delete the keyword:
public void deleteKeyword(Integer id) {
Keyword keyword = getKeyword(id);
for (User user : keyword.getUsers()) {
user.removeKeyword(keyword);
}
session.delete(keyword);
}
Note that you don't have to call update() when working with attached entities. The state of attached entities is automatically and transparently saved to the database.

Resources