add wrappers for db

This commit is contained in:
jie 2021-08-09 15:07:09 +08:00
parent b36d999300
commit 3006214897
3 changed files with 257 additions and 85 deletions

View File

@ -16,16 +16,61 @@
namespace bserv { namespace bserv {
using raw_db_connection_type = pqxx::connection;
using raw_db_transaction_type = pqxx::work;
class db_field {
private:
pqxx::field field_;
public:
db_field(const pqxx::field& field) : field_{field} {}
const char* c_str() const { return field_.c_str(); }
template <typename Type>
Type as() const { return field_.as<Type>(); }
};
class db_row {
private:
pqxx::row row_;
public:
db_row(const pqxx::row& row) : row_{row} {}
std::size_t size() const { return row_.size(); }
db_field operator[](std::size_t idx) const { return row_[idx]; }
};
class db_result {
private:
pqxx::result result_;
public:
class const_iterator {
private:
pqxx::result::const_iterator iterator_;
public:
const_iterator(
const pqxx::result::const_iterator& iterator
) : iterator_{iterator} {}
const_iterator& operator++() { ++iterator_; return *this; }
bool operator==(const const_iterator& rhs) const { return iterator_ == rhs.iterator_; }
bool operator!=(const const_iterator& rhs) const { return iterator_ != rhs.iterator_; }
db_row operator*() const { return *iterator_; }
};
db_result() = default;
db_result(const pqxx::result& result) : result_{result} {}
const_iterator begin() const { return result_.begin(); }
const_iterator end() const { return result_.end(); }
std::string query() const { return result_.query(); }
};
class db_connection_manager; class db_connection_manager;
class db_connection { class db_connection {
private: private:
db_connection_manager& mgr_; db_connection_manager& mgr_;
std::shared_ptr<pqxx::connection> conn_; std::shared_ptr<raw_db_connection_type> conn_;
public: public:
db_connection( db_connection(
db_connection_manager& mgr, db_connection_manager& mgr,
std::shared_ptr<pqxx::connection> conn) std::shared_ptr<raw_db_connection_type> conn)
: mgr_{mgr}, conn_{conn} {} : mgr_{mgr}, conn_{conn} {}
// non-copiable, non-assignable // non-copiable, non-assignable
db_connection(const db_connection&) = delete; db_connection(const db_connection&) = delete;
@ -33,13 +78,13 @@ public:
// during the destruction, it should put itself back to the // during the destruction, it should put itself back to the
// manager's queue // manager's queue
~db_connection(); ~db_connection();
pqxx::connection& get() { return *conn_; } raw_db_connection_type& get() { return *conn_; }
}; };
// provides the database connection pool functionality // provides the database connection pool functionality
class db_connection_manager { class db_connection_manager {
private: private:
std::queue<std::shared_ptr<pqxx::connection>> queue_; std::queue<std::shared_ptr<raw_db_connection_type>> queue_;
// this lock is for manipulating the `queue_` // this lock is for manipulating the `queue_`
mutable std::mutex queue_lock_; mutable std::mutex queue_lock_;
// since C++ 17 doesn't provide the semaphore functionality, // since C++ 17 doesn't provide the semaphore functionality,
@ -52,7 +97,7 @@ public:
db_connection_manager(const std::string& conn_str, int n) { db_connection_manager(const std::string& conn_str, int n) {
for (int i = 0; i < n; ++i) for (int i = 0; i < n; ++i)
queue_.emplace( queue_.emplace(
std::make_shared<pqxx::connection>(conn_str)); std::make_shared<raw_db_connection_type>(conn_str));
} }
// if there are no available database connections, this function // if there are no available database connections, this function
// blocks until there is any; // blocks until there is any;
@ -68,7 +113,7 @@ public:
// `queue_lock_` is acquired so that only one thread will // `queue_lock_` is acquired so that only one thread will
// modify the `queue_` // modify the `queue_`
std::lock_guard<std::mutex> lg{queue_lock_}; std::lock_guard<std::mutex> lg{queue_lock_};
std::shared_ptr<pqxx::connection> conn = queue_.front(); std::shared_ptr<raw_db_connection_type> conn = queue_.front();
queue_.pop(); queue_.pop();
// if there are no connections in the `queue_`, // if there are no connections in the `queue_`,
// `counter_lock_` remains to be locked // `counter_lock_` remains to be locked
@ -93,7 +138,7 @@ inline db_connection::~db_connection() {
class db_parameter { class db_parameter {
public: public:
virtual ~db_parameter() = default; virtual ~db_parameter() = default;
virtual std::string get_value(pqxx::work&) = 0; virtual std::string get_value(raw_db_transaction_type&) = 0;
}; };
class db_name : public db_parameter { class db_name : public db_parameter {
@ -102,8 +147,8 @@ private:
public: public:
db_name(const std::string& value) db_name(const std::string& value)
: value_{value} {} : value_{value} {}
std::string get_value(pqxx::work& w) { std::string get_value(raw_db_transaction_type& tx) {
return w.quote_name(value_); return tx.quote_name(value_);
} }
}; };
@ -114,7 +159,7 @@ private:
public: public:
db_value(const Type& value) db_value(const Type& value)
: value_{value} {} : value_{value} {}
std::string get_value(pqxx::work&) { std::string get_value(raw_db_transaction_type&) {
return std::to_string(value_); return std::to_string(value_);
} }
}; };
@ -126,8 +171,8 @@ private:
public: public:
db_value(const std::string& value) db_value(const std::string& value)
: value_{value} {} : value_{value} {}
std::string get_value(pqxx::work& w) { std::string get_value(raw_db_transaction_type& tx) {
return w.quote(value_); return tx.quote(value_);
} }
}; };
@ -138,7 +183,7 @@ private:
public: public:
db_value(const bool& value) db_value(const bool& value)
: value_{value} {} : value_{value} {}
std::string get_value(pqxx::work&) { std::string get_value(raw_db_transaction_type&) {
return value_ ? "true" : "false"; return value_ ? "true" : "false";
} }
}; };
@ -169,8 +214,8 @@ inline std::shared_ptr<db_parameter> convert_parameter(
template <typename ...Params> template <typename ...Params>
std::vector<std::string> convert_parameters( std::vector<std::string> convert_parameters(
pqxx::work& w, std::shared_ptr<Params>... params) { raw_db_transaction_type& tx, std::shared_ptr<Params>... params) {
return {params->get_value(w)...}; return {params->get_value(tx)...};
} }
// ************************************* // *************************************
@ -183,7 +228,7 @@ public:
: name_{name} {} : name_{name} {}
virtual ~db_field_holder() = default; virtual ~db_field_holder() = default;
virtual void add( virtual void add(
const pqxx::row& row, size_t field_idx, const db_row& row, std::size_t field_idx,
boost::json::object& obj) = 0; boost::json::object& obj) = 0;
}; };
@ -192,7 +237,7 @@ class db_field : public db_field_holder {
public: public:
using db_field_holder::db_field_holder; using db_field_holder::db_field_holder;
void add( void add(
const pqxx::row& row, size_t field_idx, const db_row& row, std::size_t field_idx,
boost::json::object& obj) { boost::json::object& obj) {
obj[name_] = row[field_idx].as<Type>(); obj[name_] = row[field_idx].as<Type>();
} }
@ -203,7 +248,7 @@ class db_field<std::string> : public db_field_holder {
public: public:
using db_field_holder::db_field_holder; using db_field_holder::db_field_holder;
void add( void add(
const pqxx::row& row, size_t field_idx, const db_row& row, std::size_t field_idx,
boost::json::object& obj) { boost::json::object& obj) {
obj[name_] = row[field_idx].c_str(); obj[name_] = row[field_idx].c_str();
} }
@ -234,63 +279,80 @@ public:
const std::initializer_list< const std::initializer_list<
std::shared_ptr<db_internal::db_field_holder>>& fields) std::shared_ptr<db_internal::db_field_holder>>& fields)
: fields_{fields} {} : fields_{fields} {}
boost::json::object convert_row(const pqxx::row& row) { boost::json::object convert_row(const db_row& row) {
boost::json::object obj; boost::json::object obj;
for (size_t i = 0; i < fields_.size(); ++i) for (std::size_t i = 0; i < fields_.size(); ++i)
fields_[i]->add(row, i, obj); fields_[i]->add(row, i, obj);
return obj; return obj;
} }
std::vector<boost::json::object> convert_to_vector( std::vector<boost::json::object> convert_to_vector(
const pqxx::result& result) { const db_result& result) {
std::vector<boost::json::object> results; std::vector<boost::json::object> results;
for (const auto& row : result) for (const auto& row : result)
results.emplace_back(convert_row(row)); results.emplace_back(convert_row(row));
return results; return results;
} }
std::optional<boost::json::object> convert_to_optional( std::optional<boost::json::object> convert_to_optional(
const pqxx::result& result) { const db_result& result) {
if (result.size() == 0) return std::nullopt; // result.size() == 0
if (result.size() == 1) return convert_row(result[0]); if (result.begin() == result.end()) return std::nullopt;
auto iterator = result.begin();
auto first = iterator;
// result.size() == 1
if (++iterator == result.end())
return convert_row(*first);
// result.size() > 1 // result.size() > 1
throw invalid_operation_exception{ throw invalid_operation_exception{
"too many objects to convert"}; "too many objects to convert"};
} }
}; };
// Usage: class db_transaction {
// db_exec(tx, "select * from ? where ? = ? and first_name = 'Name??'", private:
// db_name("auth_user"), db_name("is_active"), db_value<bool>(true)); raw_db_transaction_type tx_;
// -> SQL: select * from "auth_user" where "is_active" = true and first_name = 'Name?' public:
// ====================================================================================== db_transaction(
// db_exec(tx, "select * from ? where ? = ? and first_name = ?", std::shared_ptr<db_connection> connection_ptr
// db_name("auth_user"), db_name("is_active"), false, "Name??"); ) : tx_{connection_ptr->get()} {}
// -> SQL: select * from "auth_user" where "is_active" = false and first_name = 'Name??' // non-copiable, non-assignable
// ====================================================================================== db_transaction(const db_transaction&) = delete;
// Note: "?" is the placeholder for parameters, and "??" will be converted to "?" in SQL. db_transaction& operator=(const db_transaction&) = delete;
// But, "??" in the parameters remains. // Usage:
template <typename ...Params> // exec("select * from ? where ? = ? and first_name = 'Name??'",
pqxx::result db_exec(pqxx::work& w, // db_name("auth_user"), db_name("is_active"), db_value<bool>(true));
const std::string& s, const Params&... params) { // -> SQL: select * from "auth_user" where "is_active" = true and first_name = 'Name?'
std::vector<std::string> param_vec = // ======================================================================================
db_internal::convert_parameters( // exec("select * from ? where ? = ? and first_name = ?",
w, db_internal::convert_parameter(params)...); // db_name("auth_user"), db_name("is_active"), false, "Name??");
size_t idx = 0; // -> SQL: select * from "auth_user" where "is_active" = false and first_name = 'Name??'
std::string query; // ======================================================================================
for (size_t i = 0; i < s.length(); ++i) { // Note: "?" is the placeholder for parameters, and "??" will be converted to "?" in SQL.
if (s[i] == '?') { // But, "??" in the parameters remains.
if (i + 1 < s.length() && s[i + 1] == '?') { template <typename ...Params>
query += s[++i]; db_result exec(const std::string& s, const Params&... params) {
} else { std::vector<std::string> param_vec =
if (idx < param_vec.size()) { db_internal::convert_parameters(
query += param_vec[idx++]; tx_, db_internal::convert_parameter(params)...);
} else throw std::out_of_range{"too few parameters"}; std::size_t idx = 0;
} std::string query;
} else query += s[i]; for (std::size_t i = 0; i < s.length(); ++i) {
if (s[i] == '?') {
if (i + 1 < s.length() && s[i + 1] == '?') {
query += s[++i];
} else {
if (idx < param_vec.size()) {
query += param_vec[idx++];
} else throw std::out_of_range{"too few parameters"};
}
} else query += s[i];
}
if (idx != param_vec.size())
throw invalid_operation_exception{"too many parameters"};
return tx_.exec(query);
} }
if (idx != param_vec.size()) void commit() { tx_.commit(); }
throw invalid_operation_exception{"too many parameters"}; void abort() { tx_.abort(); }
return w.exec(query); };
}
// TODO: add support for time conversions between postgresql and c++, use timestamp? // TODO: add support for time conversions between postgresql and c++, use timestamp?

View File

@ -8,8 +8,6 @@
#include <vector> #include <vector>
#include <optional> #include <optional>
#include <pqxx/pqxx>
#include "bserv/common.hpp" #include "bserv/common.hpp"
// register an orm mapping (to convert the db query results into // register an orm mapping (to convert the db query results into
@ -35,17 +33,17 @@ bserv::db_relation_to_object orm_user{
}; };
std::optional<boost::json::object> get_user( std::optional<boost::json::object> get_user(
pqxx::work& tx, bserv::db_transaction& tx,
const std::string& username) { const std::string& username) {
pqxx::result r = bserv::db_exec(tx, bserv::db_result r = tx.exec(
"select * from auth_user where username = ?", username); "select * from auth_user where username = ?", username);
lginfo << r.query(); // this is how you log info lginfo << r.query(); // this is how you log info
return orm_user.convert_to_optional(r); return orm_user.convert_to_optional(r);
} }
std::string get_or_empty( std::string get_or_empty(
boost::json::object& obj, boost::json::object& obj,
const std::string& key) { const std::string& key) {
return obj.count(key) ? obj[key].as_string().c_str() : ""; return obj.count(key) ? obj[key].as_string().c_str() : "";
} }
@ -53,15 +51,25 @@ std::string get_or_empty(
// the return type should be `std::nullopt_t`, // the return type should be `std::nullopt_t`,
// and the return value should be `std::nullopt`. // and the return value should be `std::nullopt`.
std::nullopt_t hello( std::nullopt_t hello(
bserv::response_type& response, bserv::response_type& response,
std::shared_ptr<bserv::session_type> session_ptr) { std::shared_ptr<bserv::session_type> session_ptr) {
bserv::session_type& session = *session_ptr; bserv::session_type& session = *session_ptr;
boost::json::object obj; boost::json::object obj;
if (session.count("user")) { if (session.count("user")) {
// NOTE: modifications to sessions must be performed
// BEFORE referencing objects in them. this is because
// modifications might invalidate referenced objects.
// in this example, "count" might be added to `session`,
// which should be performed first.
// then `user` can be referenced safely.
if (!session.count("count")) {
session["count"] = 0;
}
auto& user = session["user"].as_object(); auto& user = session["user"].as_object();
session["count"] = session["count"].as_int64() + 1;
obj = { obj = {
{"msg", std::string{"welcome, "} {"welcome", user["username"]},
+ user["username"].as_string().c_str() + "!"} {"count", session["count"]}
}; };
} else { } else {
obj = {{"msg", "hello, world!"}}; obj = {{"msg", "hello, world!"}};
@ -76,11 +84,11 @@ std::nullopt_t hello(
// if you return a json object, the serialization // if you return a json object, the serialization
// is performed automatically. // is performed automatically.
boost::json::object user_register( boost::json::object user_register(
bserv::request_type& request, bserv::request_type& request,
// the json object is obtained from the request body, // the json object is obtained from the request body,
// as well as the url parameters // as well as the url parameters
boost::json::object&& params, boost::json::object&& params,
std::shared_ptr<bserv::db_connection> conn) { std::shared_ptr<bserv::db_connection> conn) {
if (request.method() != boost::beast::http::verb::post) { if (request.method() != boost::beast::http::verb::post) {
throw bserv::url_not_found_exception{}; throw bserv::url_not_found_exception{};
} }
@ -97,7 +105,7 @@ boost::json::object user_register(
}; };
} }
auto username = params["username"].as_string(); auto username = params["username"].as_string();
pqxx::work tx{conn->get()}; bserv::db_transaction tx{conn};
auto opt_user = get_user(tx, username.c_str()); auto opt_user = get_user(tx, username.c_str());
if (opt_user.has_value()) { if (opt_user.has_value()) {
return { return {
@ -106,7 +114,7 @@ boost::json::object user_register(
}; };
} }
auto password = params["password"].as_string(); auto password = params["password"].as_string();
pqxx::result r = bserv::db_exec(tx, bserv::db_result r = tx.exec(
"insert into ? " "insert into ? "
"(?, password, is_superuser, " "(?, password, is_superuser, "
"first_name, last_name, email, is_active) values " "first_name, last_name, email, is_active) values "
@ -127,10 +135,10 @@ boost::json::object user_register(
} }
boost::json::object user_login( boost::json::object user_login(
bserv::request_type& request, bserv::request_type& request,
boost::json::object&& params, boost::json::object&& params,
std::shared_ptr<bserv::db_connection> conn, std::shared_ptr<bserv::db_connection> conn,
std::shared_ptr<bserv::session_type> session_ptr) { std::shared_ptr<bserv::session_type> session_ptr) {
if (request.method() != boost::beast::http::verb::post) { if (request.method() != boost::beast::http::verb::post) {
throw bserv::url_not_found_exception{}; throw bserv::url_not_found_exception{};
} }
@ -147,7 +155,7 @@ boost::json::object user_login(
}; };
} }
auto username = params["username"].as_string(); auto username = params["username"].as_string();
pqxx::work tx{conn->get()}; bserv::db_transaction tx{conn};
auto opt_user = get_user(tx, username.c_str()); auto opt_user = get_user(tx, username.c_str());
if (!opt_user.has_value()) { if (!opt_user.has_value()) {
return { return {
@ -180,9 +188,9 @@ boost::json::object user_login(
} }
boost::json::object find_user( boost::json::object find_user(
std::shared_ptr<bserv::db_connection> conn, std::shared_ptr<bserv::db_connection> conn,
const std::string& username) { const std::string& username) {
pqxx::work tx{conn->get()}; bserv::db_transaction tx{conn};
auto user = get_user(tx, username); auto user = get_user(tx, username);
if (!user.has_value()) { if (!user.has_value()) {
return { return {
@ -190,6 +198,7 @@ boost::json::object find_user(
{"message", "requested user does not exist"} {"message", "requested user does not exist"}
}; };
} }
user.value().erase("id");
user.value().erase("password"); user.value().erase("password");
return { return {
{"success", true}, {"success", true},
@ -198,7 +207,7 @@ boost::json::object find_user(
} }
boost::json::object user_logout( boost::json::object user_logout(
std::shared_ptr<bserv::session_type> session_ptr) { std::shared_ptr<bserv::session_type> session_ptr) {
bserv::session_type& session = *session_ptr; bserv::session_type& session = *session_ptr;
if (session.count("user")) { if (session.count("user")) {
session.erase("user"); session.erase("user");
@ -235,7 +244,7 @@ boost::json::object send_request(
} }
boost::json::object echo( boost::json::object echo(
boost::json::object&& params) { boost::json::object&& params) {
return {{"echo", params}}; return {{"echo", params}};
} }

101
scripts/db_test.py Normal file
View File

@ -0,0 +1,101 @@
import uuid
import string
import secrets
import random
import requests
from multiprocessing import Process
from pprint import pprint
from time import time
char_string = string.ascii_letters + string.digits
def get_password(n):
return ''.join(secrets.choice(char_string) for _ in range(n))
def get_string(n):
return ''.join(random.choice(char_string) for _ in range(n))
def create_user():
return {
"username": str(uuid.uuid4()),
"password": get_password(16),
"first_name": get_string(5),
"last_name": get_string(5),
"email": get_string(5) + "@" + get_string(5) + ".com"
}
# pprint(create_user())
# exit()
def session_test():
session = requests.session()
user = create_user()
res = session.post("http://localhost:8080").json()
if res != {'msg': 'hello, world!'}:
print('test failed')
# print(res)
res = session.post("http://localhost:8080/register", json=user).json()
if res != {'success': True, 'message': 'user registered'}:
print('test failed')
# print(res)
res = session.post("http://localhost:8080/login", json={
"username": user["username"],
"password": user["password"]
}).json()
if res != {'success': True, 'message': 'login successfully'}:
print('test failed')
# print(res)
n = random.randint(1, 5)
for i in range(1, n + 1):
res = session.post("http://localhost:8080").json()
if res != {'welcome': user["username"], 'count': i}:
print('test failed')
# print(res)
res = session.post("http://localhost:8080/find/" + user["username"]).json()
if res != {
'success': True,
'user': {
'username': user["username"],
'is_active': True,
'is_superuser': False,
'first_name': user["first_name"],
'last_name': user["last_name"],
'email': user["email"]
}}:
print('test failed')
# print(res)
res = session.post("http://localhost:8080/logout").json()
if res != {'success': True, 'message': 'logout successfully'}:
print('test failed')
# print(res)
res = session.post("http://localhost:8080").json()
if res != {'msg': 'hello, world!'}:
print('test failed')
# print(res)
# session_test()
# exit()
P = 1000 # number of concurrent processes
processes = [Process(target=session_test) for i in range(P)]
print('starting')
start = time()
for p in processes:
p.start()
for p in processes:
p.join()
end = time()
print('test ended')
print('elapsed: ', end - start)