Hi im trying to delete user by his id from a table in Postgres database.
Im using Spring and R2DBC and trying to use DatabaseClient.execute("sqlCommand") for my custom delete query:
import org.springframework.data.r2dbc.core.DatabaseClient;
#Service
#Transactional
public class CustomSqlService {
private final DatabaseClient databaseClient;
public Mono<Void> deleteWithCustomSql(String sql) {
databaseClient.execute(sql)
return Mono.empty();
}
Where sql is "DELETE FROM user_table WHERE user_id = 1;"
Method in the Controller:
#RestController
#RequestMapping("postgres/")
#Timed
public class PostgresController {
// omitted code
#DeleteMapping(path = "/delete_user/{userId}")
public Mono<Void> deleteUser(#PathVariable("userId") Long userId) {
return customSqlService.deleteWithCustomSql("DELETE FROM user_table WHERE user_id = " + userId);
}
But when I test it, command is not working. When i debug i can see there's MonoOnResumeError in the result from .execute().
I have other methods that perform insert and select statements in the same fashion and they work well.
The test I have for it:
#Test
void shouldDeleteDataFromTable() {
User user = User.builder()
.userId(1L)
.sessionId(2L)
.timestamp(10L)
.build();
webTestClient
.post()
.uri("/postgres/save_user")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(user), User.class)
.exchange()
.expectStatus().isOk()
webTestClient
.delete()
.uri("/postgres/delete_user/1")
.exchange()
.expectStatus()
.isOk();
userRepository.findAll().take(1).as(StepVerifier::create)
.expectNextCount(0)
.verifyComplete();
How to correctly use databaseClient.execute() for custom delete query in PostgreSQL ?
Hope you are using the latest R2dbc 1.0 and Spring Data R2dbc(managed by Spring Boot 3.0).
Your method deleteWithCustomSql does not work. There is no subscription on the databaseCLient.exectue, the sql is never be executed and return a result.
Try to change to the following, move sql here, and use bind to bind a parameter to sql.
public Mono<Long> deleteByUserId(Long userId) {
return databaseClient.sql("DELETE FROM user_table WHERE user_id = :userId")
.bind("userId", userId)
.fetch()
.rowsUpdated();
}
In the controller, changed to the following.
#DeleteMapping(path = "/users/{userId}")
public Mono<ResponseEntity> deleteUser(#PathVariable("userId") Long userId) {
return customSqlService.deleteByUserId(userId)
.map(deleted -> {
if(deleted>0) return noContent().build();
else return notFound().build();
});
}
Check my working example of delete operations, which demonstrates how to bind a parameter in sql, it is based on the latest Spring Data R2dbc/Postgres.
Related
We currently have the following, working soft delete customizer in place:
public class SoftDeleteCustomizer implements DescriptorCustomizer {
#Override
public void customize(ClassDescriptor descriptor) {
descriptor.getQueryManager().setDeleteSQLString(
String.format("UPDATE %s SET record_status = 'D', record_status_time = CURRENT_TIMESTAMP WHERE id = #ID",
descriptor.getTableName()
)
);
}
}
We now want to add the user that deleted the record. I could sanitize the username, but I would prefer to use a parameter / argument.
I rewrote the customizer and did not set an argument for the #ID, as it was already injected correctly somewhere. I then found out that it was not injected when you are using a DeleteObjectQuery (with arguments?). So I have to add an argument for the #ID it seems, but I don't know how to get the id / primary key value of the record / entity to be deleted from a ClassDescriptor.
This is what I have so far:
#Override
public void customize(final ClassDescriptor descriptor) {
final DeleteObjectQuery query = new DeleteObjectQuery();
query.addArgument("DELETED_BY", String.class);
query.addArgument("ID", Long.class);
query.addArgumentValue(SecurityUtils.getUsername());
query.addArgumentValue(...); // How to get the ID of the record to delete?
query.setSQLString(String.format(DELETE_SQL, descriptor.getTableName()));
descriptor.getQueryManager().setDeleteQuery(query);
}
Okay, as a workaround I used our audit listener which we added as one of the EntityListeners. It implements SessionCustomizer. There I was able to do:
#Override
public void postDelete(final DescriptorEvent event) {
final Long id = ((AbstractEntity) event.getObject()).getId();
// Create and execute update query to set the username
}
I am learning JPA, I found out that we have some functions which is already present in Jparepository like save,saveAll,find, findAll etc. but there is nothing like update,
I come across one scenario where I need to update the table, if the value is already present otherwise I need to insert the record in table.
I created
#Repository
public interface ProductInfoRepository
extends JpaRepository<ProductInfoTable, String>
{
Optional<ProductInfoTable> findByProductName(String productname);
}
public class ProductServiceImpl
implements ProductService
{
#Autowired
private ProductInfoRepository productRepository;
#Override
public ResponseMessage saveProductDetail(ProductInfo productInfo)
{
Optional<ProductInfoTable> productInfoinTable =
productRepository.findByProductName(productInfo.getProductName());
ProductInfoTable productInfoDetail;
Integer quantity = productInfo.getQuantity();
if (productInfoinTable.isPresent())
{
quantity += productInfoinTable.get().getQuantity();
}
productInfoDetail =
new ProductInfoTable(productInfo.getProductName(), quantity + productInfo.getQuantity(),
productInfo.getImage());
productRepository.save(productInfoDetail);
return new ResponseMessage("product saved successfully");
}
}
as you can see, I can save the record if the record is new, but when I am trying to save the record which is already present in table it is giving me error related to primarykeyviolation which is obvious. I checked somewhat, we can do the update by creating the entitymanager object or jpa query but what if I dont want to use both of them. is there any other way we can do so ?
update I also added the instance of EntityManager and trying to merge the code
#Override
public ResponseMessage saveProductDetail(ProductInfo productInfo)
{
Optional<ProductInfoTable> productInfoinTable =
productRepository.findByProductName(productInfo.getProductName());
ProductInfoTable productInfoDetail;
Integer price = productInfo.getPrice();
if (productInfoinTable.isPresent())
{
price = productInfoinTable.get().getPrice();
}
productInfoDetail =
new ProductInfoTable(productInfo.getProductName(), price, productInfo.getImage());
em.merge(productInfoDetail);
return new ResponseMessage("product saved successfully");
but no error, no execution of update statements in log, any possible reasons for that ?
}
I suspect you need code like this to solve the problem
public ResponseMessage saveProductDetail(ProductInfo productInfo)
{
Optional<ProductInfoTable> productInfoinTable =
productRepository.findByProductName(productInfo.getProductName());
final ProductInfoTable productInfoDetail;
if (productInfoinTable.isPresent()) {
// to edit
productInfoDetail = productInfoinTable.get();
Integer quantity = productInfoDetail.getQuantity() + productInfo.getQuantity();
productInfoDetail.setQuantity(quantity);
} else {
// to create new
productInfoDetail = new ProductInfoTable(productInfo.getProductName(),
productInfo.getQuantity(), productInfo.getImage());
}
productRepository.save(productInfoDetail);
return new ResponseMessage("product saved successfully");
}
I know pagination is somewhat against reactive principles, but due to requirements I have to make it work somehow. I'm using Spring Data 2.1.6 and I can't upgrade so ReactiveQuerydslSpecification for the dynamic query is out of the question. I figured I could use ReactiveMongoTemplate so I came up with this:
public interface IPersonRepository extends ReactiveMongoRepository<Person, String>, IPersonFilterRepository {
Flux<Person> findAllByCarId(String carId);
}
public interface IPersonFilterRepository {
Flux<Person> findAllByCarIdAndCreatedDateBetween(String carId, PersonStatus status,
OffsetDateTime from, OffsetDateTime to,
Pageable pageable);
}
#Repository
public class PersonFilterRepository implements IPersonFilterRepository {
#Autowired
private ReactiveMongoTemplate reactiveMongoTemplate;
#Override
public Flux<Person> findAllByCarIdAndCreatedDateBetween(String carId, PersonStatus status,
OffsetDateTime from, OffsetDateTime to,
Pageable pageable) {
Query query = new Query(Criteria.where("carId").is(carId));
if (status != null) {
query.addCriteria(Criteria.where("status").is(status));
}
OffsetDateTime maxLimit = OffsetDateTime.now(ZoneOffset.UTC).minusMonths(3).withDayOfMonth(1); // beginning of month
if (from == null || from.isBefore(maxLimit)) {
from = maxLimit;
}
query.addCriteria(Criteria.where("createdDateTime").gte(from));
if (to == null) {
to = OffsetDateTime.now(ZoneOffset.UTC);
}
query.addCriteria(Criteria.where("createdDateTime").lte(to));
// problem is trying to come up with a decent page-ish behavior compatible with Flux
/*return reactiveMongoTemplate.count(query, Person.class)
.flatMap(count -> reactiveMongoTemplate.find(query, Person.class)
.flatMap(p -> new PageImpl<Person>(p, pageable, count))
.collectList()
.map());*/
/* return reactiveMongoTemplate.find(query, Person.class)
.buffer(pageable.getPageSize(), pageable.getPageNumber() + 1)
//.elementAt(pageable.getPageNumber(), new ArrayList<>())
.thenMany(Flux::from);*/
}
I've tried to return a Page<Person> (assuming for once this single method could be non-reactive, for once) and it fails with the following error while running testing (Spring context does not load successfully due to: InvalidDataAccessApiUsageException: 'IDocumentFilterRepository.findAllByCustomerIdAndCreatedDateBetween' must not use sliced or paged execution. Please use Flux.buffer(size, skip). I've also tried returning Mono<Page<Person>> and then fails with "Method has to use a either multi-item reactive wrapper return type or a wrapped Page/Slice type. Offending method: 'IDocumentFilterRepository.findAllByCustomerIdAndCreatedDateBetween', so I guess my only option is returning a Flux, according to Example 133, snippet 3
Turns out you can just add the following to the query object:
query.with(pageable);
reactiveMongoTemplate.find(query, Person.class);
Return Flux<T> and it will work out of the box.
I created a web api project using .net core and entity framework.
This uses a stored procedure which returns back most of the properties of a database table defined by entity framework.
The entity framwrok does not bring back all the columns of the table. And I get an error when I call the api complaining it cannot find the missing columns when I execute the stored procedure using ,
_context.Set<TableFromSql>().FromSql("execute dbo.spr_GetValue").ToList();
I created another model class which defines the properties brought back from the SP( called NewClass).
_context.Set<NewClass>().FromSql("execute dbo.spr_GetValue").ToList();
This works, but just wanted to check if there is a convention that the SP should only return the model classes from the database.
The SQL query must return data for all properties of the entity or query type
For this limitation, it is caused when mapping the sql query result to Model. It loop through the properties in model and try to retrive the values from query result. If the model properties are not exist in query result, it will throw error.
If you want to return required columns instead of all columns, one options is to define the returned model by Query.
For your demo code, you may define this in OnModelCreating.
builder.Query<TableFromSql>();
Note, for this way, you need to make sure all properties in TableFromSql exist in execute dbo.spr_GetValue.
For another way, you may implement your own FromSql which will add condition to check whether the properties are exist in query result.
public static class DbContextExtensions
{
public static List<T> RawSqlQuery<T>(this DbContext context,string query)
{
using (var command = context.Database.GetDbConnection().CreateCommand())
{
command.CommandText = query;
command.CommandType = CommandType.Text;
context.Database.OpenConnection();
using (var result = command.ExecuteReader())
{
var entities = new List<T>();
return DataReaderMapToList<T>(result);
}
}
}
public static List<T> DataReaderMapToList<T>(IDataReader dr)
{
List<T> list = new List<T>();
T obj = default(T);
while (dr.Read())
{
obj = Activator.CreateInstance<T>();
foreach (PropertyInfo prop in obj.GetType().GetProperties())
{
if (ColumnExists(dr, prop.Name))
{
if (!object.Equals(dr[prop.Name], DBNull.Value))
{
prop.SetValue(obj, dr[prop.Name], null);
}
}
}
list.Add(obj);
}
return list;
}
public static bool ColumnExists(IDataReader reader, string columnName)
{
return reader.GetSchemaTable()
.Rows
.OfType<DataRow>()
.Any(row => row["ColumnName"].ToString() == columnName);
}
}
Use above code like :
var result = _context.RawSqlQuery<ToDoItemVM>("execute [dbo].[get_TodoItem]");
I need to execute a Raw SQL query that returns multiple result sets. Is that possible in EF CF ?
Thanks!
Description
Yes you can! ;) You can perform a raw sql query using DbCommand in Entity Framework Code First too.
You can perform queries with multiple result sets and jump to the next result set using NextResult() method of the SqlDataReader class.
Sample
namespace MyNamespace
{
public class MyDbContext : DbContext
{
}
public class Test
{
public void Test()
{
MyDbContext context = new MyDbContext();
DbCommand db = context.Database.Connection.CreateCommand();
db.CommandText = "SELECT propertie1 FROM Table1; SELECT propertie1 from Table2";
try
{
DbDataReader reader = db.ExecuteReader();
while (reader.Read())
{
// read your data from result set 1
string value = Convert.ToString(reader["propertie1"]);
}
reader.NextResult();
while (reader.Read())
{
// read your data from result set 2
string value = Convert.ToString(reader["propertie1"]);
}
}
catch
{
// Exception handling
}
finally
{
if (db.Connection.State != ConnectionState.Closed)
db.Connection.Close();
}
}
}
}
More Information
Using DbContext in EF 4.1 Part 10: Raw SQL Queries